pgsql.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. // +build !nopgsql
  2. package dataprovider
  3. import (
  4. "context"
  5. "crypto/x509"
  6. "database/sql"
  7. "errors"
  8. "fmt"
  9. "strings"
  10. "time"
  11. // we import lib/pq here to be able to disable PostgreSQL support using a build tag
  12. _ "github.com/lib/pq"
  13. "github.com/drakkan/sftpgo/logger"
  14. "github.com/drakkan/sftpgo/version"
  15. "github.com/drakkan/sftpgo/vfs"
  16. )
  17. const (
  18. pgsqlInitial = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL);
  19. CREATE TABLE "{{admins}}" ("id" serial NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE,
  20. "password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL,
  21. "filters" text NULL, "additional_info" text NULL);
  22. CREATE TABLE "{{folders}}" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE,
  23. "path" varchar(512) NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL,
  24. "last_quota_update" bigint NOT NULL);
  25. CREATE TABLE "{{users}}" ("id" serial NOT NULL PRIMARY KEY, "status" integer NOT NULL, "expiration_date" bigint NOT NULL,
  26. "username" varchar(255) NOT NULL UNIQUE, "password" text NULL, "public_keys" text NULL, "home_dir" varchar(512) NOT NULL,
  27. "uid" integer NOT NULL, "gid" integer NOT NULL, "max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL,
  28. "quota_files" integer NOT NULL, "permissions" text NOT NULL, "used_quota_size" bigint NOT NULL,
  29. "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL,
  30. "download_bandwidth" integer NOT NULL, "last_login" bigint NOT NULL, "filters" text NULL, "filesystem" text NULL,
  31. "additional_info" text NULL);
  32. CREATE TABLE "{{folders_mapping}}" ("id" serial NOT NULL PRIMARY KEY, "virtual_path" varchar(512) NOT NULL,
  33. "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "folder_id" integer NOT NULL, "user_id" integer NOT NULL);
  34. ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "{{prefix}}unique_mapping" UNIQUE ("user_id", "folder_id");
  35. ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "{{prefix}}folders_mapping_folder_id_fk_folders_id"
  36. FOREIGN KEY ("folder_id") REFERENCES "{{folders}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
  37. ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "{{prefix}}folders_mapping_user_id_fk_users_id"
  38. FOREIGN KEY ("user_id") REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
  39. CREATE INDEX "{{prefix}}folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
  40. CREATE INDEX "{{prefix}}folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
  41. INSERT INTO {{schema_version}} (version) VALUES (8);
  42. `
  43. pgsqlV9SQL = `ALTER TABLE "{{admins}}" ADD COLUMN "description" varchar(512) NULL;
  44. ALTER TABLE "{{folders}}" ADD COLUMN "description" varchar(512) NULL;
  45. ALTER TABLE "{{folders}}" ADD COLUMN "filesystem" text NULL;
  46. ALTER TABLE "{{users}}" ADD COLUMN "description" varchar(512) NULL;
  47. `
  48. pgsqlV9DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "description" CASCADE;
  49. ALTER TABLE "{{folders}}" DROP COLUMN "filesystem" CASCADE;
  50. ALTER TABLE "{{folders}}" DROP COLUMN "description" CASCADE;
  51. ALTER TABLE "{{admins}}" DROP COLUMN "description" CASCADE;
  52. `
  53. )
  54. // PGSQLProvider auth provider for PostgreSQL database
  55. type PGSQLProvider struct {
  56. dbHandle *sql.DB
  57. }
  58. func init() {
  59. version.AddFeature("+pgsql")
  60. }
  61. func initializePGSQLProvider() error {
  62. var err error
  63. dbHandle, err := sql.Open("postgres", getPGSQLConnectionString(false))
  64. if err == nil {
  65. providerLog(logger.LevelDebug, "postgres database handle created, connection string: %#v, pool size: %v",
  66. getPGSQLConnectionString(true), config.PoolSize)
  67. dbHandle.SetMaxOpenConns(config.PoolSize)
  68. if config.PoolSize > 0 {
  69. dbHandle.SetMaxIdleConns(config.PoolSize)
  70. } else {
  71. dbHandle.SetMaxIdleConns(2)
  72. }
  73. dbHandle.SetConnMaxLifetime(240 * time.Second)
  74. provider = &PGSQLProvider{dbHandle: dbHandle}
  75. } else {
  76. providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v",
  77. getPGSQLConnectionString(true), err)
  78. }
  79. return err
  80. }
  81. func getPGSQLConnectionString(redactedPwd bool) string {
  82. var connectionString string
  83. if config.ConnectionString == "" {
  84. password := config.Password
  85. if redactedPwd {
  86. password = "[redacted]"
  87. }
  88. connectionString = fmt.Sprintf("host='%v' port=%v dbname='%v' user='%v' password='%v' sslmode=%v connect_timeout=10",
  89. config.Host, config.Port, config.Name, config.Username, password, getSSLMode())
  90. } else {
  91. connectionString = config.ConnectionString
  92. }
  93. return connectionString
  94. }
  95. func (p *PGSQLProvider) checkAvailability() error {
  96. return sqlCommonCheckAvailability(p.dbHandle)
  97. }
  98. func (p *PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
  99. return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
  100. }
  101. func (p *PGSQLProvider) validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error) {
  102. return sqlCommonValidateUserAndTLSCertificate(username, protocol, tlsCert, p.dbHandle)
  103. }
  104. func (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
  105. return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
  106. }
  107. func (p *PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
  108. return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
  109. }
  110. func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
  111. return sqlCommonGetUsedQuota(username, p.dbHandle)
  112. }
  113. func (p *PGSQLProvider) updateLastLogin(username string) error {
  114. return sqlCommonUpdateLastLogin(username, p.dbHandle)
  115. }
  116. func (p *PGSQLProvider) userExists(username string) (User, error) {
  117. return sqlCommonGetUserByUsername(username, p.dbHandle)
  118. }
  119. func (p *PGSQLProvider) addUser(user *User) error {
  120. return sqlCommonAddUser(user, p.dbHandle)
  121. }
  122. func (p *PGSQLProvider) updateUser(user *User) error {
  123. return sqlCommonUpdateUser(user, p.dbHandle)
  124. }
  125. func (p *PGSQLProvider) deleteUser(user *User) error {
  126. return sqlCommonDeleteUser(user, p.dbHandle)
  127. }
  128. func (p *PGSQLProvider) dumpUsers() ([]User, error) {
  129. return sqlCommonDumpUsers(p.dbHandle)
  130. }
  131. func (p *PGSQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
  132. return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
  133. }
  134. func (p *PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
  135. return sqlCommonDumpFolders(p.dbHandle)
  136. }
  137. func (p *PGSQLProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {
  138. return sqlCommonGetFolders(limit, offset, order, p.dbHandle)
  139. }
  140. func (p *PGSQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {
  141. ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
  142. defer cancel()
  143. return sqlCommonGetFolderByName(ctx, name, p.dbHandle)
  144. }
  145. func (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
  146. return sqlCommonAddFolder(folder, p.dbHandle)
  147. }
  148. func (p *PGSQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
  149. return sqlCommonUpdateFolder(folder, p.dbHandle)
  150. }
  151. func (p *PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
  152. return sqlCommonDeleteFolder(folder, p.dbHandle)
  153. }
  154. func (p *PGSQLProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {
  155. return sqlCommonUpdateFolderQuota(name, filesAdd, sizeAdd, reset, p.dbHandle)
  156. }
  157. func (p *PGSQLProvider) getUsedFolderQuota(name string) (int, int64, error) {
  158. return sqlCommonGetFolderUsedQuota(name, p.dbHandle)
  159. }
  160. func (p *PGSQLProvider) adminExists(username string) (Admin, error) {
  161. return sqlCommonGetAdminByUsername(username, p.dbHandle)
  162. }
  163. func (p *PGSQLProvider) addAdmin(admin *Admin) error {
  164. return sqlCommonAddAdmin(admin, p.dbHandle)
  165. }
  166. func (p *PGSQLProvider) updateAdmin(admin *Admin) error {
  167. return sqlCommonUpdateAdmin(admin, p.dbHandle)
  168. }
  169. func (p *PGSQLProvider) deleteAdmin(admin *Admin) error {
  170. return sqlCommonDeleteAdmin(admin, p.dbHandle)
  171. }
  172. func (p *PGSQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
  173. return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
  174. }
  175. func (p *PGSQLProvider) dumpAdmins() ([]Admin, error) {
  176. return sqlCommonDumpAdmins(p.dbHandle)
  177. }
  178. func (p *PGSQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
  179. return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
  180. }
  181. func (p *PGSQLProvider) close() error {
  182. return p.dbHandle.Close()
  183. }
  184. func (p *PGSQLProvider) reloadConfig() error {
  185. return nil
  186. }
  187. // initializeDatabase creates the initial database structure
  188. func (p *PGSQLProvider) initializeDatabase() error {
  189. dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
  190. if err == nil && dbVersion.Version > 0 {
  191. return ErrNoInitRequired
  192. }
  193. initialSQL := strings.ReplaceAll(pgsqlInitial, "{{schema_version}}", sqlTableSchemaVersion)
  194. initialSQL = strings.ReplaceAll(initialSQL, "{{admins}}", sqlTableAdmins)
  195. initialSQL = strings.ReplaceAll(initialSQL, "{{folders}}", sqlTableFolders)
  196. initialSQL = strings.ReplaceAll(initialSQL, "{{users}}", sqlTableUsers)
  197. initialSQL = strings.ReplaceAll(initialSQL, "{{folders_mapping}}", sqlTableFoldersMapping)
  198. initialSQL = strings.ReplaceAll(initialSQL, "{{prefix}}", config.SQLTablesPrefix)
  199. if config.Driver == CockroachDataProviderName {
  200. // Cockroach does not support deferrable constraint validation, we don't need it,
  201. // we keep these definitions for the PostgreSQL driver to avoid changes for users
  202. // upgrading from old SFTPGo versions
  203. initialSQL = strings.ReplaceAll(initialSQL, "DEFERRABLE INITIALLY DEFERRED", "")
  204. }
  205. return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 8)
  206. }
  207. func (p *PGSQLProvider) migrateDatabase() error {
  208. dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
  209. if err != nil {
  210. return err
  211. }
  212. switch version := dbVersion.Version; {
  213. case version == sqlDatabaseVersion:
  214. providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", version)
  215. return ErrNoInitRequired
  216. case version < 8:
  217. err = fmt.Errorf("database version %v is too old, please see the upgrading docs", version)
  218. providerLog(logger.LevelError, "%v", err)
  219. logger.ErrorToConsole("%v", err)
  220. return err
  221. case version == 8:
  222. return updatePGSQLDatabaseFromV8(p.dbHandle)
  223. case version == 9:
  224. return updatePGSQLDatabaseFromV9(p.dbHandle)
  225. default:
  226. if version > sqlDatabaseVersion {
  227. providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
  228. sqlDatabaseVersion)
  229. logger.WarnToConsole("database version %v is newer than the supported one: %v", version,
  230. sqlDatabaseVersion)
  231. return nil
  232. }
  233. return fmt.Errorf("database version not handled: %v", version)
  234. }
  235. }
  236. func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
  237. dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
  238. if err != nil {
  239. return err
  240. }
  241. if dbVersion.Version == targetVersion {
  242. return errors.New("current version match target version, nothing to do")
  243. }
  244. switch dbVersion.Version {
  245. case 9:
  246. return downgradePGSQLDatabaseFromV9(p.dbHandle)
  247. case 10:
  248. return downgradePGSQLDatabaseFromV10(p.dbHandle)
  249. default:
  250. return fmt.Errorf("database version not handled: %v", dbVersion.Version)
  251. }
  252. }
  253. func updatePGSQLDatabaseFromV8(dbHandle *sql.DB) error {
  254. if err := updatePGSQLDatabaseFrom8To9(dbHandle); err != nil {
  255. return err
  256. }
  257. return updatePGSQLDatabaseFromV9(dbHandle)
  258. }
  259. func updatePGSQLDatabaseFromV9(dbHandle *sql.DB) error {
  260. return updatePGSQLDatabaseFrom9To10(dbHandle)
  261. }
  262. func downgradePGSQLDatabaseFromV9(dbHandle *sql.DB) error {
  263. return downgradePGSQLDatabaseFrom9To8(dbHandle)
  264. }
  265. func downgradePGSQLDatabaseFromV10(dbHandle *sql.DB) error {
  266. if err := downgradePGSQLDatabaseFrom10To9(dbHandle); err != nil {
  267. return err
  268. }
  269. return downgradePGSQLDatabaseFromV9(dbHandle)
  270. }
  271. func updatePGSQLDatabaseFrom8To9(dbHandle *sql.DB) error {
  272. logger.InfoToConsole("updating database version: 8 -> 9")
  273. providerLog(logger.LevelInfo, "updating database version: 8 -> 9")
  274. sql := strings.ReplaceAll(pgsqlV9SQL, "{{users}}", sqlTableUsers)
  275. sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
  276. sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
  277. return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 9)
  278. }
  279. func downgradePGSQLDatabaseFrom9To8(dbHandle *sql.DB) error {
  280. logger.InfoToConsole("downgrading database version: 9 -> 8")
  281. providerLog(logger.LevelInfo, "downgrading database version: 9 -> 8")
  282. sql := strings.ReplaceAll(pgsqlV9DownSQL, "{{users}}", sqlTableUsers)
  283. sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
  284. sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
  285. return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 8)
  286. }
  287. func updatePGSQLDatabaseFrom9To10(dbHandle *sql.DB) error {
  288. return sqlCommonUpdateDatabaseFrom9To10(dbHandle)
  289. }
  290. func downgradePGSQLDatabaseFrom10To9(dbHandle *sql.DB) error {
  291. return sqlCommonDowngradeDatabaseFrom10To9(dbHandle)
  292. }