1// Package dataprovider provides data access. 2// It abstracts different data providers and exposes a common API. 3package dataprovider 4 5import ( 6 "bufio" 7 "bytes" 8 "context" 9 "crypto/sha1" 10 "crypto/sha256" 11 "crypto/sha512" 12 "crypto/subtle" 13 "crypto/x509" 14 "encoding/base64" 15 "encoding/json" 16 "errors" 17 "fmt" 18 "hash" 19 "io" 20 "net" 21 "net/http" 22 "net/url" 23 "os" 24 "os/exec" 25 "path" 26 "path/filepath" 27 "regexp" 28 "runtime" 29 "strconv" 30 "strings" 31 "sync" 32 "sync/atomic" 33 "time" 34 35 "github.com/GehirnInc/crypt" 36 "github.com/GehirnInc/crypt/apr1_crypt" 37 "github.com/GehirnInc/crypt/md5_crypt" 38 "github.com/GehirnInc/crypt/sha512_crypt" 39 "github.com/alexedwards/argon2id" 40 "github.com/go-chi/render" 41 "github.com/rs/xid" 42 passwordvalidator "github.com/wagslane/go-password-validator" 43 "golang.org/x/crypto/bcrypt" 44 "golang.org/x/crypto/pbkdf2" 45 "golang.org/x/crypto/ssh" 46 47 "github.com/drakkan/sftpgo/v2/httpclient" 48 "github.com/drakkan/sftpgo/v2/kms" 49 "github.com/drakkan/sftpgo/v2/logger" 50 "github.com/drakkan/sftpgo/v2/metric" 51 "github.com/drakkan/sftpgo/v2/mfa" 52 "github.com/drakkan/sftpgo/v2/sdk" 53 "github.com/drakkan/sftpgo/v2/sdk/plugin" 54 "github.com/drakkan/sftpgo/v2/util" 55 "github.com/drakkan/sftpgo/v2/vfs" 56) 57 58const ( 59 // SQLiteDataProviderName defines the name for SQLite database provider 60 SQLiteDataProviderName = "sqlite" 61 // PGSQLDataProviderName defines the name for PostgreSQL database provider 62 PGSQLDataProviderName = "postgresql" 63 // MySQLDataProviderName defines the name for MySQL database provider 64 MySQLDataProviderName = "mysql" 65 // BoltDataProviderName defines the name for bbolt key/value store provider 66 BoltDataProviderName = "bolt" 67 // MemoryDataProviderName defines the name for memory provider 68 MemoryDataProviderName = "memory" 69 // CockroachDataProviderName defines the for CockroachDB provider 70 CockroachDataProviderName = "cockroachdb" 71 // DumpVersion defines the version for the dump. 72 // For restore/load we support the current version and the previous one 73 DumpVersion = 10 74 75 argonPwdPrefix = "$argon2id$" 76 bcryptPwdPrefix = "$2a$" 77 pbkdf2SHA1Prefix = "$pbkdf2-sha1$" 78 pbkdf2SHA256Prefix = "$pbkdf2-sha256$" 79 pbkdf2SHA512Prefix = "$pbkdf2-sha512$" 80 pbkdf2SHA256B64SaltPrefix = "$pbkdf2-b64salt-sha256$" 81 md5cryptPwdPrefix = "$1$" 82 md5cryptApr1PwdPrefix = "$apr1$" 83 sha512cryptPwdPrefix = "$6$" 84 trackQuotaDisabledError = "please enable track_quota in your configuration to use this method" 85 operationAdd = "add" 86 operationUpdate = "update" 87 operationDelete = "delete" 88 sqlPrefixValidChars = "abcdefghijklmnopqrstuvwxyz_0123456789" 89 maxHookResponseSize = 1048576 // 1MB 90) 91 92// Supported algorithms for hashing passwords. 93// These algorithms can be used when SFTPGo hashes a plain text password 94const ( 95 HashingAlgoBcrypt = "bcrypt" 96 HashingAlgoArgon2ID = "argon2id" 97) 98 99// ordering constants 100const ( 101 OrderASC = "ASC" 102 OrderDESC = "DESC" 103) 104 105const ( 106 protocolSSH = "SSH" 107 protocolFTP = "FTP" 108 protocolWebDAV = "DAV" 109 protocolHTTP = "HTTP" 110) 111 112var ( 113 // SupportedProviders defines the supported data providers 114 SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName, 115 BoltDataProviderName, MemoryDataProviderName, CockroachDataProviderName} 116 // ValidPerms defines all the valid permissions for a user 117 ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermCreateDirs, PermRename, 118 PermRenameFiles, PermRenameDirs, PermDelete, PermDeleteFiles, PermDeleteDirs, PermCreateSymlinks, PermChmod, 119 PermChown, PermChtimes} 120 // ValidLoginMethods defines all the valid login methods 121 ValidLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodKeyboardInteractive, 122 SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt, LoginMethodTLSCertificate, 123 LoginMethodTLSCertificateAndPwd} 124 // SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications 125 SSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt} 126 // ErrNoAuthTryed defines the error for connection closed before authentication 127 ErrNoAuthTryed = errors.New("no auth tryed") 128 // ValidProtocols defines all the valid protcols 129 ValidProtocols = []string{protocolSSH, protocolFTP, protocolWebDAV, protocolHTTP} 130 // MFAProtocols defines the supported protocols for multi-factor authentication 131 MFAProtocols = []string{protocolHTTP, protocolSSH, protocolFTP} 132 // ErrNoInitRequired defines the error returned by InitProvider if no inizialization/update is required 133 ErrNoInitRequired = errors.New("the data provider is up to date") 134 // ErrInvalidCredentials defines the error to return if the supplied credentials are invalid 135 ErrInvalidCredentials = errors.New("invalid credentials") 136 ErrLoginNotAllowedFromIP = errors.New("login is not allowed from this IP") 137 isAdminCreated = int32(0) 138 validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)} 139 config Config 140 provider Provider 141 sqlPlaceholders []string 142 internalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix} 143 hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, 144 pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix} 145 pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix} 146 pbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix} 147 unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix} 148 sharedProviders = []string{PGSQLDataProviderName, MySQLDataProviderName, CockroachDataProviderName} 149 logSender = "dataProvider" 150 availabilityTicker *time.Ticker 151 availabilityTickerDone chan bool 152 updateCachesTicker *time.Ticker 153 updateCachesTickerDone chan bool 154 lastCachesUpdate int64 155 credentialsDirPath string 156 sqlTableUsers = "users" 157 sqlTableFolders = "folders" 158 sqlTableFoldersMapping = "folders_mapping" 159 sqlTableAdmins = "admins" 160 sqlTableAPIKeys = "api_keys" 161 sqlTableShares = "shares" 162 sqlTableSchemaVersion = "schema_version" 163 argon2Params *argon2id.Params 164 lastLoginMinDelay = 10 * time.Minute 165 usernameRegex = regexp.MustCompile("^[a-zA-Z0-9-_.~]+$") 166 tempPath string 167) 168 169type schemaVersion struct { 170 Version int 171} 172 173// BcryptOptions defines the options for bcrypt password hashing 174type BcryptOptions struct { 175 Cost int `json:"cost" mapstructure:"cost"` 176} 177 178// Argon2Options defines the options for argon2 password hashing 179type Argon2Options struct { 180 Memory uint32 `json:"memory" mapstructure:"memory"` 181 Iterations uint32 `json:"iterations" mapstructure:"iterations"` 182 Parallelism uint8 `json:"parallelism" mapstructure:"parallelism"` 183} 184 185// PasswordHashing defines the configuration for password hashing 186type PasswordHashing struct { 187 BcryptOptions BcryptOptions `json:"bcrypt_options" mapstructure:"bcrypt_options"` 188 Argon2Options Argon2Options `json:"argon2_options" mapstructure:"argon2_options"` 189 // Algorithm to use for hashing passwords. Available algorithms: argon2id, bcrypt. Default: bcrypt 190 Algo string `json:"algo" mapstructure:"algo"` 191} 192 193// PasswordValidationRules defines the password validation rules 194type PasswordValidationRules struct { 195 // MinEntropy defines the minimum password entropy. 196 // 0 means disabled, any password will be accepted. 197 // Take a look at the following link for more details 198 // https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use 199 MinEntropy float64 `json:"min_entropy" mapstructure:"min_entropy"` 200} 201 202// PasswordValidation defines the password validation rules for admins and protocol users 203type PasswordValidation struct { 204 // Password validation rules for SFTPGo admin users 205 Admins PasswordValidationRules `json:"admins" mapstructure:"admins"` 206 // Password validation rules for SFTPGo protocol users 207 Users PasswordValidationRules `json:"users" mapstructure:"users"` 208} 209 210// ObjectsActions defines the action to execute on user create, update, delete for the specified objects 211type ObjectsActions struct { 212 // Valid values are add, update, delete. Empty slice to disable 213 ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"` 214 // Valid values are user, admin, api_key 215 ExecuteFor []string `json:"execute_for" mapstructure:"execute_for"` 216 // Absolute path to an external program or an HTTP URL 217 Hook string `json:"hook" mapstructure:"hook"` 218} 219 220// ProviderStatus defines the provider status 221type ProviderStatus struct { 222 Driver string `json:"driver"` 223 IsActive bool `json:"is_active"` 224 Error string `json:"error"` 225} 226 227// Config provider configuration 228type Config struct { 229 // Driver name, must be one of the SupportedProviders 230 Driver string `json:"driver" mapstructure:"driver"` 231 // Database name. For driver sqlite this can be the database name relative to the config dir 232 // or the absolute path to the SQLite database. 233 Name string `json:"name" mapstructure:"name"` 234 // Database host 235 Host string `json:"host" mapstructure:"host"` 236 // Database port 237 Port int `json:"port" mapstructure:"port"` 238 // Database username 239 Username string `json:"username" mapstructure:"username"` 240 // Database password 241 Password string `json:"password" mapstructure:"password"` 242 // Used for drivers mysql and postgresql. 243 // 0 disable SSL/TLS connections. 244 // 1 require ssl. 245 // 2 set ssl mode to verify-ca for driver postgresql and skip-verify for driver mysql. 246 // 3 set ssl mode to verify-full for driver postgresql and preferred for driver mysql. 247 SSLMode int `json:"sslmode" mapstructure:"sslmode"` 248 // Custom database connection string. 249 // If not empty this connection string will be used instead of build one using the previous parameters 250 ConnectionString string `json:"connection_string" mapstructure:"connection_string"` 251 // prefix for SQL tables 252 SQLTablesPrefix string `json:"sql_tables_prefix" mapstructure:"sql_tables_prefix"` 253 // Set the preferred way to track users quota between the following choices: 254 // 0, disable quota tracking. REST API to scan user dir and update quota will do nothing 255 // 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions 256 // 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions 257 // and for virtual folders. 258 // With this configuration the "quota scan" REST API can still be used to periodically update space usage 259 // for users without quota restrictions 260 TrackQuota int `json:"track_quota" mapstructure:"track_quota"` 261 // Sets the maximum number of open connections for mysql and postgresql driver. 262 // Default 0 (unlimited) 263 PoolSize int `json:"pool_size" mapstructure:"pool_size"` 264 // Users default base directory. 265 // If no home dir is defined while adding a new user, and this value is 266 // a valid absolute path, then the user home dir will be automatically 267 // defined as the path obtained joining the base dir and the username 268 UsersBaseDir string `json:"users_base_dir" mapstructure:"users_base_dir"` 269 // Actions to execute on objects add, update, delete. 270 // The supported objects are user, admin, api_key. 271 // Update action will not be fired for internal updates such as the last login or the user quota fields. 272 Actions ObjectsActions `json:"actions" mapstructure:"actions"` 273 // Absolute path to an external program or an HTTP URL to invoke for users authentication. 274 // Leave empty to use builtin authentication. 275 // If the authentication succeed the user will be automatically added/updated inside the defined data provider. 276 // Actions defined for user added/updated will not be executed in this case. 277 // This method is slower than built-in authentication methods, but it's very flexible as anyone can 278 // easily write his own authentication hooks. 279 ExternalAuthHook string `json:"external_auth_hook" mapstructure:"external_auth_hook"` 280 // ExternalAuthScope defines the scope for the external authentication hook. 281 // - 0 means all supported authentication scopes, the external hook will be executed for password, 282 // public key, keyboard interactive authentication and TLS certificates 283 // - 1 means passwords only 284 // - 2 means public keys only 285 // - 4 means keyboard interactive only 286 // - 8 means TLS certificates only 287 // you can combine the scopes, for example 3 means password and public key, 5 password and keyboard 288 // interactive and so on 289 ExternalAuthScope int `json:"external_auth_scope" mapstructure:"external_auth_scope"` 290 // CredentialsPath defines the directory for storing user provided credential files such as 291 // Google Cloud Storage credentials. It can be a path relative to the config dir or an 292 // absolute path 293 CredentialsPath string `json:"credentials_path" mapstructure:"credentials_path"` 294 // Absolute path to an external program or an HTTP URL to invoke just before the user login. 295 // This program/URL allows to modify or create the user trying to login. 296 // It is useful if you have users with dynamic fields to update just before the login. 297 // Please note that if you want to create a new user, the pre-login hook response must 298 // include all the mandatory user fields. 299 // 300 // The pre-login hook must finish within 30 seconds. 301 // 302 // If an error happens while executing the "PreLoginHook" then login will be denied. 303 // PreLoginHook and ExternalAuthHook are mutally exclusive. 304 // Leave empty to disable. 305 PreLoginHook string `json:"pre_login_hook" mapstructure:"pre_login_hook"` 306 // Absolute path to an external program or an HTTP URL to invoke after the user login. 307 // Based on the configured scope you can choose if notify failed or successful logins 308 // or both 309 PostLoginHook string `json:"post_login_hook" mapstructure:"post_login_hook"` 310 // PostLoginScope defines the scope for the post-login hook. 311 // - 0 means notify both failed and successful logins 312 // - 1 means notify failed logins 313 // - 2 means notify successful logins 314 PostLoginScope int `json:"post_login_scope" mapstructure:"post_login_scope"` 315 // Absolute path to an external program or an HTTP URL to invoke just before password 316 // authentication. This hook allows you to externally check the provided password, 317 // its main use case is to allow to easily support things like password+OTP for protocols 318 // without keyboard interactive support such as FTP and WebDAV. You can ask your users 319 // to login using a string consisting of a fixed password and a One Time Token, you 320 // can verify the token inside the hook and ask to SFTPGo to verify the fixed part. 321 CheckPasswordHook string `json:"check_password_hook" mapstructure:"check_password_hook"` 322 // CheckPasswordScope defines the scope for the check password hook. 323 // - 0 means all protocols 324 // - 1 means SSH 325 // - 2 means FTP 326 // - 4 means WebDAV 327 // you can combine the scopes, for example 6 means FTP and WebDAV 328 CheckPasswordScope int `json:"check_password_scope" mapstructure:"check_password_scope"` 329 // Defines how the database will be initialized/updated: 330 // - 0 means automatically 331 // - 1 means manually using the initprovider sub-command 332 UpdateMode int `json:"update_mode" mapstructure:"update_mode"` 333 // PasswordHashing defines the configuration for password hashing 334 PasswordHashing PasswordHashing `json:"password_hashing" mapstructure:"password_hashing"` 335 // PreferDatabaseCredentials indicates whether credential files (currently used for Google 336 // Cloud Storage) should be stored in the database instead of in the directory specified by 337 // CredentialsPath. 338 PreferDatabaseCredentials bool `json:"prefer_database_credentials" mapstructure:"prefer_database_credentials"` 339 // SkipNaturalKeysValidation allows to use any UTF-8 character for natural keys as username, admin name, 340 // folder name. These keys are used in URIs for REST API and Web admin. By default only unreserved URI 341 // characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". 342 SkipNaturalKeysValidation bool `json:"skip_natural_keys_validation" mapstructure:"skip_natural_keys_validation"` 343 // PasswordValidation defines the password validation rules 344 PasswordValidation PasswordValidation `json:"password_validation" mapstructure:"password_validation"` 345 // Verifying argon2 passwords has a high memory and computational cost, 346 // by enabling, in memory, password caching you reduce this cost. 347 PasswordCaching bool `json:"password_caching" mapstructure:"password_caching"` 348 // DelayedQuotaUpdate defines the number of seconds to accumulate quota updates. 349 // If there are a lot of close uploads, accumulating quota updates can save you many 350 // queries to the data provider. 351 // If you want to track quotas, a scheduled quota update is recommended in any case, the stored 352 // quota size may be incorrect for several reasons, such as an unexpected shutdown, temporary provider 353 // failures, file copied outside of SFTPGo, and so on. 354 // 0 means immediate quota update. 355 DelayedQuotaUpdate int `json:"delayed_quota_update" mapstructure:"delayed_quota_update"` 356 // If enabled, a default admin user with username "admin" and password "password" will be created 357 // on first start. 358 // You can also create the first admin user by using the web interface or by loading initial data. 359 CreateDefaultAdmin bool `json:"create_default_admin" mapstructure:"create_default_admin"` 360 // If the data provider is shared across multiple SFTPGo instances, set this parameter to 1. 361 // MySQL, PostgreSQL and CockroachDB can be shared, this setting is ignored for other data 362 // providers. For shared data providers, SFTPGo periodically reloads the latest updated users, 363 // based on the "updated_at" field, and updates its internal caches if users are updated from 364 // a different instance. This check, if enabled, is executed every 10 minutes 365 IsShared int `json:"is_shared" mapstructure:"is_shared"` 366} 367 368// BackupData defines the structure for the backup/restore files 369type BackupData struct { 370 Users []User `json:"users"` 371 Folders []vfs.BaseVirtualFolder `json:"folders"` 372 Admins []Admin `json:"admins"` 373 APIKeys []APIKey `json:"api_keys"` 374 Shares []Share `json:"shares"` 375 Version int `json:"version"` 376} 377 378// HasFolder returns true if the folder with the given name is included 379func (d *BackupData) HasFolder(name string) bool { 380 for _, folder := range d.Folders { 381 if folder.Name == name { 382 return true 383 } 384 } 385 return false 386} 387 388type checkPasswordRequest struct { 389 Username string `json:"username"` 390 IP string `json:"ip"` 391 Password string `json:"password"` 392 Protocol string `json:"protocol"` 393} 394 395type checkPasswordResponse struct { 396 // 0 KO, 1 OK, 2 partial success, -1 not executed 397 Status int `json:"status"` 398 // for status = 2 this is the password to check against the one stored 399 // inside the SFTPGo data provider 400 ToVerify string `json:"to_verify"` 401} 402 403// GetQuotaTracking returns the configured mode for user's quota tracking 404func GetQuotaTracking() int { 405 return config.TrackQuota 406} 407 408// Provider defines the interface that data providers must implement. 409type Provider interface { 410 validateUserAndPass(username, password, ip, protocol string) (User, error) 411 validateUserAndPubKey(username string, pubKey []byte) (User, string, error) 412 validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error) 413 updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error 414 getUsedQuota(username string) (int, int64, error) 415 userExists(username string) (User, error) 416 addUser(user *User) error 417 updateUser(user *User) error 418 deleteUser(user *User) error 419 getUsers(limit int, offset int, order string) ([]User, error) 420 dumpUsers() ([]User, error) 421 getRecentlyUpdatedUsers(after int64) ([]User, error) 422 updateLastLogin(username string) error 423 updateAdminLastLogin(username string) error 424 setUpdatedAt(username string) 425 getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) 426 getFolderByName(name string) (vfs.BaseVirtualFolder, error) 427 addFolder(folder *vfs.BaseVirtualFolder) error 428 updateFolder(folder *vfs.BaseVirtualFolder) error 429 deleteFolder(folder *vfs.BaseVirtualFolder) error 430 updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error 431 getUsedFolderQuota(name string) (int, int64, error) 432 dumpFolders() ([]vfs.BaseVirtualFolder, error) 433 adminExists(username string) (Admin, error) 434 addAdmin(admin *Admin) error 435 updateAdmin(admin *Admin) error 436 deleteAdmin(admin *Admin) error 437 getAdmins(limit int, offset int, order string) ([]Admin, error) 438 dumpAdmins() ([]Admin, error) 439 validateAdminAndPass(username, password, ip string) (Admin, error) 440 apiKeyExists(keyID string) (APIKey, error) 441 addAPIKey(apiKey *APIKey) error 442 updateAPIKey(apiKey *APIKey) error 443 deleteAPIKey(apiKey *APIKey) error 444 getAPIKeys(limit int, offset int, order string) ([]APIKey, error) 445 dumpAPIKeys() ([]APIKey, error) 446 updateAPIKeyLastUse(keyID string) error 447 shareExists(shareID, username string) (Share, error) 448 addShare(share *Share) error 449 updateShare(share *Share) error 450 deleteShare(share *Share) error 451 getShares(limit int, offset int, order, username string) ([]Share, error) 452 dumpShares() ([]Share, error) 453 updateShareLastUse(shareID string, numTokens int) error 454 checkAvailability() error 455 close() error 456 reloadConfig() error 457 initializeDatabase() error 458 migrateDatabase() error 459 revertDatabase(targetVersion int) error 460 resetDatabase() error 461} 462 463// SetTempPath sets the path for temporary files 464func SetTempPath(fsPath string) { 465 tempPath = fsPath 466} 467 468// Initialize the data provider. 469// An error is returned if the configured driver is invalid or if the data provider cannot be initialized 470func Initialize(cnf Config, basePath string, checkAdmins bool) error { 471 var err error 472 config = cnf 473 474 if filepath.IsAbs(config.CredentialsPath) { 475 credentialsDirPath = config.CredentialsPath 476 } else { 477 credentialsDirPath = filepath.Join(basePath, config.CredentialsPath) 478 } 479 vfs.SetCredentialsDirPath(credentialsDirPath) 480 481 if err = initializeHashingAlgo(&cnf); err != nil { 482 return err 483 } 484 485 if err = validateHooks(); err != nil { 486 return err 487 } 488 err = createProvider(basePath) 489 if err != nil { 490 return err 491 } 492 if cnf.UpdateMode == 0 { 493 err = provider.initializeDatabase() 494 if err != nil && err != ErrNoInitRequired { 495 logger.WarnToConsole("Unable to initialize data provider: %v", err) 496 providerLog(logger.LevelWarn, "Unable to initialize data provider: %v", err) 497 return err 498 } 499 if err == nil { 500 logger.DebugToConsole("Data provider successfully initialized") 501 } 502 err = provider.migrateDatabase() 503 if err != nil && err != ErrNoInitRequired { 504 providerLog(logger.LevelWarn, "database migration error: %v", err) 505 return err 506 } 507 if checkAdmins && cnf.CreateDefaultAdmin { 508 err = checkDefaultAdmin() 509 if err != nil { 510 providerLog(logger.LevelWarn, "check default admin error: %v", err) 511 return err 512 } 513 } 514 } else { 515 providerLog(logger.LevelInfo, "database initialization/migration skipped, manual mode is configured") 516 } 517 admins, err := provider.getAdmins(1, 0, OrderASC) 518 if err != nil { 519 return err 520 } 521 atomic.StoreInt32(&isAdminCreated, int32(len(admins))) 522 startAvailabilityTimer() 523 startUpdateCachesTimer() 524 delayedQuotaUpdater.start() 525 return nil 526} 527 528func validateHooks() error { 529 var hooks []string 530 if config.PreLoginHook != "" && !strings.HasPrefix(config.PreLoginHook, "http") { 531 hooks = append(hooks, config.PreLoginHook) 532 } 533 if config.ExternalAuthHook != "" && !strings.HasPrefix(config.ExternalAuthHook, "http") { 534 hooks = append(hooks, config.ExternalAuthHook) 535 } 536 if config.PostLoginHook != "" && !strings.HasPrefix(config.PostLoginHook, "http") { 537 hooks = append(hooks, config.PostLoginHook) 538 } 539 if config.CheckPasswordHook != "" && !strings.HasPrefix(config.CheckPasswordHook, "http") { 540 hooks = append(hooks, config.CheckPasswordHook) 541 } 542 543 for _, hook := range hooks { 544 if !filepath.IsAbs(hook) { 545 return fmt.Errorf("invalid hook: %#v must be an absolute path", hook) 546 } 547 _, err := os.Stat(hook) 548 if err != nil { 549 providerLog(logger.LevelWarn, "invalid hook: %v", err) 550 return err 551 } 552 } 553 554 return nil 555} 556 557func initializeHashingAlgo(cnf *Config) error { 558 argon2Params = &argon2id.Params{ 559 Memory: cnf.PasswordHashing.Argon2Options.Memory, 560 Iterations: cnf.PasswordHashing.Argon2Options.Iterations, 561 Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism, 562 SaltLength: 16, 563 KeyLength: 32, 564 } 565 566 if config.PasswordHashing.Algo == HashingAlgoBcrypt { 567 if config.PasswordHashing.BcryptOptions.Cost > bcrypt.MaxCost { 568 err := fmt.Errorf("invalid bcrypt cost %v, max allowed %v", config.PasswordHashing.BcryptOptions.Cost, bcrypt.MaxCost) 569 logger.WarnToConsole("Unable to initialize data provider: %v", err) 570 providerLog(logger.LevelWarn, "Unable to initialize data provider: %v", err) 571 return err 572 } 573 } 574 return nil 575} 576 577func validateSQLTablesPrefix() error { 578 if config.SQLTablesPrefix != "" { 579 for _, char := range config.SQLTablesPrefix { 580 if !strings.Contains(sqlPrefixValidChars, strings.ToLower(string(char))) { 581 return errors.New("invalid sql_tables_prefix only chars in range 'a..z', 'A..Z', '0-9' and '_' are allowed") 582 } 583 } 584 sqlTableUsers = config.SQLTablesPrefix + sqlTableUsers 585 sqlTableFolders = config.SQLTablesPrefix + sqlTableFolders 586 sqlTableFoldersMapping = config.SQLTablesPrefix + sqlTableFoldersMapping 587 sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins 588 sqlTableAPIKeys = config.SQLTablesPrefix + sqlTableAPIKeys 589 sqlTableShares = config.SQLTablesPrefix + sqlTableShares 590 sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion 591 providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v admins %#v "+ 592 "api keys %#v shares %#v schema version %#v", sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, 593 sqlTableAdmins, sqlTableAPIKeys, sqlTableShares, sqlTableSchemaVersion) 594 } 595 return nil 596} 597 598func checkDefaultAdmin() error { 599 admins, err := provider.getAdmins(1, 0, OrderASC) 600 if err != nil { 601 return err 602 } 603 if len(admins) > 0 { 604 return nil 605 } 606 logger.Debug(logSender, "", "no admins found, try to create the default one") 607 // we need to create the default admin 608 admin := &Admin{} 609 if err := admin.setFromEnv(); err != nil { 610 return err 611 } 612 return provider.addAdmin(admin) 613} 614 615// InitializeDatabase creates the initial database structure 616func InitializeDatabase(cnf Config, basePath string) error { 617 config = cnf 618 619 if filepath.IsAbs(config.CredentialsPath) { 620 credentialsDirPath = config.CredentialsPath 621 } else { 622 credentialsDirPath = filepath.Join(basePath, config.CredentialsPath) 623 } 624 625 err := createProvider(basePath) 626 if err != nil { 627 return err 628 } 629 err = provider.initializeDatabase() 630 if err != nil && err != ErrNoInitRequired { 631 return err 632 } 633 return provider.migrateDatabase() 634} 635 636// RevertDatabase restores schema and/or data to a previous version 637func RevertDatabase(cnf Config, basePath string, targetVersion int) error { 638 config = cnf 639 640 if filepath.IsAbs(config.CredentialsPath) { 641 credentialsDirPath = config.CredentialsPath 642 } else { 643 credentialsDirPath = filepath.Join(basePath, config.CredentialsPath) 644 } 645 646 err := createProvider(basePath) 647 if err != nil { 648 return err 649 } 650 err = provider.initializeDatabase() 651 if err != nil && err != ErrNoInitRequired { 652 return err 653 } 654 return provider.revertDatabase(targetVersion) 655} 656 657// ResetDatabase restores schema and/or data to a previous version 658func ResetDatabase(cnf Config, basePath string) error { 659 config = cnf 660 661 if filepath.IsAbs(config.CredentialsPath) { 662 credentialsDirPath = config.CredentialsPath 663 } else { 664 credentialsDirPath = filepath.Join(basePath, config.CredentialsPath) 665 } 666 667 if err := createProvider(basePath); err != nil { 668 return err 669 } 670 return provider.resetDatabase() 671} 672 673// CheckAdminAndPass validates the given admin and password connecting from ip 674func CheckAdminAndPass(username, password, ip string) (Admin, error) { 675 return provider.validateAdminAndPass(username, password, ip) 676} 677 678// CheckCachedUserCredentials checks the credentials for a cached user 679func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protocol string, tlsCert *x509.Certificate) error { 680 if loginMethod != LoginMethodPassword { 681 _, err := checkUserAndTLSCertificate(&user.User, protocol, tlsCert) 682 if err != nil { 683 return err 684 } 685 if loginMethod == LoginMethodTLSCertificate { 686 if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) { 687 return fmt.Errorf("certificate login method is not allowed for user %#v", user.User.Username) 688 } 689 return nil 690 } 691 } 692 if err := user.User.CheckLoginConditions(); err != nil { 693 return err 694 } 695 if password == "" { 696 return ErrInvalidCredentials 697 } 698 if user.Password != "" { 699 if password == user.Password { 700 return nil 701 } 702 } else { 703 if ok, _ := isPasswordOK(&user.User, password); ok { 704 return nil 705 } 706 } 707 return ErrInvalidCredentials 708} 709 710// CheckCompositeCredentials checks multiple credentials. 711// WebDAV users can send both a password and a TLS certificate within the same request 712func CheckCompositeCredentials(username, password, ip, loginMethod, protocol string, tlsCert *x509.Certificate) (User, string, error) { 713 if loginMethod == LoginMethodPassword { 714 user, err := CheckUserAndPass(username, password, ip, protocol) 715 return user, loginMethod, err 716 } 717 user, err := CheckUserBeforeTLSAuth(username, ip, protocol, tlsCert) 718 if err != nil { 719 return user, loginMethod, err 720 } 721 if !user.IsTLSUsernameVerificationEnabled() { 722 // for backward compatibility with 2.0.x we only check the password and change the login method here 723 // in future updates we have to return an error 724 user, err := CheckUserAndPass(username, password, ip, protocol) 725 return user, LoginMethodPassword, err 726 } 727 user, err = checkUserAndTLSCertificate(&user, protocol, tlsCert) 728 if err != nil { 729 return user, loginMethod, err 730 } 731 if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) { 732 return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username) 733 } 734 if loginMethod == LoginMethodTLSCertificateAndPwd { 735 if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) { 736 user, err = doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword) 737 } else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) { 738 user, err = doExternalAuth(username, password, nil, "", ip, protocol, nil) 739 } else if config.PreLoginHook != "" { 740 user, err = executePreLoginHook(username, LoginMethodPassword, ip, protocol) 741 } 742 if err != nil { 743 return user, loginMethod, err 744 } 745 user, err = checkUserAndPass(&user, password, ip, protocol) 746 } 747 return user, loginMethod, err 748} 749 750// CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS 751func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) { 752 if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) { 753 return doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate) 754 } 755 if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) { 756 return doExternalAuth(username, "", nil, "", ip, protocol, tlsCert) 757 } 758 if config.PreLoginHook != "" { 759 return executePreLoginHook(username, LoginMethodTLSCertificate, ip, protocol) 760 } 761 return UserExists(username) 762} 763 764// CheckUserAndTLSCert returns the SFTPGo user with the given username and check if the 765// given TLS certificate allow authentication without password 766func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) { 767 if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) { 768 user, err := doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate) 769 if err != nil { 770 return user, err 771 } 772 return checkUserAndTLSCertificate(&user, protocol, tlsCert) 773 } 774 if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) { 775 user, err := doExternalAuth(username, "", nil, "", ip, protocol, tlsCert) 776 if err != nil { 777 return user, err 778 } 779 return checkUserAndTLSCertificate(&user, protocol, tlsCert) 780 } 781 if config.PreLoginHook != "" { 782 user, err := executePreLoginHook(username, LoginMethodTLSCertificate, ip, protocol) 783 if err != nil { 784 return user, err 785 } 786 return checkUserAndTLSCertificate(&user, protocol, tlsCert) 787 } 788 return provider.validateUserAndTLSCert(username, protocol, tlsCert) 789} 790 791// CheckUserAndPass retrieves the SFTPGo user with the given username and password if a match is found or an error 792func CheckUserAndPass(username, password, ip, protocol string) (User, error) { 793 if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) { 794 user, err := doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword) 795 if err != nil { 796 return user, err 797 } 798 return checkUserAndPass(&user, password, ip, protocol) 799 } 800 if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) { 801 user, err := doExternalAuth(username, password, nil, "", ip, protocol, nil) 802 if err != nil { 803 return user, err 804 } 805 return checkUserAndPass(&user, password, ip, protocol) 806 } 807 if config.PreLoginHook != "" { 808 user, err := executePreLoginHook(username, LoginMethodPassword, ip, protocol) 809 if err != nil { 810 return user, err 811 } 812 return checkUserAndPass(&user, password, ip, protocol) 813 } 814 return provider.validateUserAndPass(username, password, ip, protocol) 815} 816 817// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error 818func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (User, string, error) { 819 if plugin.Handler.HasAuthScope(plugin.AuthScopePublicKey) { 820 user, err := doPluginAuth(username, "", pubKey, ip, protocol, nil, plugin.AuthScopePublicKey) 821 if err != nil { 822 return user, "", err 823 } 824 return checkUserAndPubKey(&user, pubKey) 825 } 826 if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) { 827 user, err := doExternalAuth(username, "", pubKey, "", ip, protocol, nil) 828 if err != nil { 829 return user, "", err 830 } 831 return checkUserAndPubKey(&user, pubKey) 832 } 833 if config.PreLoginHook != "" { 834 user, err := executePreLoginHook(username, SSHLoginMethodPublicKey, ip, protocol) 835 if err != nil { 836 return user, "", err 837 } 838 return checkUserAndPubKey(&user, pubKey) 839 } 840 return provider.validateUserAndPubKey(username, pubKey) 841} 842 843// CheckKeyboardInteractiveAuth checks the keyboard interactive authentication and returns 844// the authenticated user or an error 845func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) { 846 var user User 847 var err error 848 if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) { 849 user, err = doPluginAuth(username, "", nil, ip, protocol, nil, plugin.AuthScopeKeyboardInteractive) 850 } else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) { 851 user, err = doExternalAuth(username, "", nil, "1", ip, protocol, nil) 852 } else if config.PreLoginHook != "" { 853 user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol) 854 } else { 855 user, err = provider.userExists(username) 856 } 857 if err != nil { 858 return user, err 859 } 860 return doKeyboardInteractiveAuth(&user, authHook, client, ip, protocol) 861} 862 863// UpdateShareLastUse updates the LastUseAt and UsedTokens for the given share 864func UpdateShareLastUse(share *Share, numTokens int) error { 865 return provider.updateShareLastUse(share.ShareID, numTokens) 866} 867 868// UpdateAPIKeyLastUse updates the LastUseAt field for the given API key 869func UpdateAPIKeyLastUse(apiKey *APIKey) error { 870 lastUse := util.GetTimeFromMsecSinceEpoch(apiKey.LastUseAt) 871 diff := -time.Until(lastUse) 872 if diff < 0 || diff > lastLoginMinDelay { 873 return provider.updateAPIKeyLastUse(apiKey.KeyID) 874 } 875 return nil 876} 877 878// UpdateLastLogin updates the last login field for the given SFTPGo user 879func UpdateLastLogin(user *User) { 880 lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin) 881 diff := -time.Until(lastLogin) 882 if diff < 0 || diff > lastLoginMinDelay { 883 err := provider.updateLastLogin(user.Username) 884 if err == nil { 885 webDAVUsersCache.updateLastLogin(user.Username) 886 } 887 } 888} 889 890// UpdateAdminLastLogin updates the last login field for the given SFTPGo admin 891func UpdateAdminLastLogin(admin *Admin) { 892 lastLogin := util.GetTimeFromMsecSinceEpoch(admin.LastLogin) 893 diff := -time.Until(lastLogin) 894 if diff < 0 || diff > lastLoginMinDelay { 895 provider.updateAdminLastLogin(admin.Username) //nolint:errcheck 896 } 897} 898 899// UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd. 900// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference. 901func UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error { 902 if config.TrackQuota == 0 { 903 return util.NewMethodDisabledError(trackQuotaDisabledError) 904 } else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() { 905 return nil 906 } 907 if filesAdd == 0 && sizeAdd == 0 && !reset { 908 return nil 909 } 910 if config.DelayedQuotaUpdate == 0 || reset { 911 if reset { 912 delayedQuotaUpdater.resetUserQuota(user.Username) 913 } 914 return provider.updateQuota(user.Username, filesAdd, sizeAdd, reset) 915 } 916 delayedQuotaUpdater.updateUserQuota(user.Username, filesAdd, sizeAdd) 917 return nil 918} 919 920// UpdateVirtualFolderQuota updates the quota for the given virtual folder adding filesAdd and sizeAdd. 921// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference. 922func UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, sizeAdd int64, reset bool) error { 923 if config.TrackQuota == 0 { 924 return util.NewMethodDisabledError(trackQuotaDisabledError) 925 } 926 if filesAdd == 0 && sizeAdd == 0 && !reset { 927 return nil 928 } 929 if config.DelayedQuotaUpdate == 0 || reset { 930 if reset { 931 delayedQuotaUpdater.resetFolderQuota(vfolder.Name) 932 } 933 return provider.updateFolderQuota(vfolder.Name, filesAdd, sizeAdd, reset) 934 } 935 delayedQuotaUpdater.updateFolderQuota(vfolder.Name, filesAdd, sizeAdd) 936 return nil 937} 938 939// GetUsedQuota returns the used quota for the given SFTP user. 940func GetUsedQuota(username string) (int, int64, error) { 941 if config.TrackQuota == 0 { 942 return 0, 0, util.NewMethodDisabledError(trackQuotaDisabledError) 943 } 944 files, size, err := provider.getUsedQuota(username) 945 if err != nil { 946 return files, size, err 947 } 948 delayedFiles, delayedSize := delayedQuotaUpdater.getUserPendingQuota(username) 949 return files + delayedFiles, size + delayedSize, err 950} 951 952// GetUsedVirtualFolderQuota returns the used quota for the given virtual folder. 953func GetUsedVirtualFolderQuota(name string) (int, int64, error) { 954 if config.TrackQuota == 0 { 955 return 0, 0, util.NewMethodDisabledError(trackQuotaDisabledError) 956 } 957 files, size, err := provider.getUsedFolderQuota(name) 958 if err != nil { 959 return files, size, err 960 } 961 delayedFiles, delayedSize := delayedQuotaUpdater.getFolderPendingQuota(name) 962 return files + delayedFiles, size + delayedSize, err 963} 964 965// AddShare adds a new share 966func AddShare(share *Share, executor, ipAddress string) error { 967 err := provider.addShare(share) 968 if err == nil { 969 executeAction(operationAdd, executor, ipAddress, actionObjectShare, share.ShareID, share) 970 } 971 return err 972} 973 974// UpdateShare updates an existing share 975func UpdateShare(share *Share, executor, ipAddress string) error { 976 err := provider.updateShare(share) 977 if err == nil { 978 executeAction(operationUpdate, executor, ipAddress, actionObjectShare, share.ShareID, share) 979 } 980 return err 981} 982 983// DeleteShare deletes an existing share 984func DeleteShare(shareID string, executor, ipAddress string) error { 985 share, err := provider.shareExists(shareID, executor) 986 if err != nil { 987 return err 988 } 989 err = provider.deleteShare(&share) 990 if err == nil { 991 executeAction(operationDelete, executor, ipAddress, actionObjectShare, shareID, &share) 992 } 993 return err 994} 995 996// ShareExists returns the share with the given ID if it exists 997func ShareExists(shareID, username string) (Share, error) { 998 if shareID == "" { 999 return Share{}, util.NewRecordNotFoundError(fmt.Sprintf("Share %#v does not exist", shareID)) 1000 } 1001 return provider.shareExists(shareID, username) 1002} 1003 1004// AddAPIKey adds a new API key 1005func AddAPIKey(apiKey *APIKey, executor, ipAddress string) error { 1006 err := provider.addAPIKey(apiKey) 1007 if err == nil { 1008 executeAction(operationAdd, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, apiKey) 1009 } 1010 return err 1011} 1012 1013// UpdateAPIKey updates an existing API key 1014func UpdateAPIKey(apiKey *APIKey, executor, ipAddress string) error { 1015 err := provider.updateAPIKey(apiKey) 1016 if err == nil { 1017 executeAction(operationUpdate, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, apiKey) 1018 } 1019 return err 1020} 1021 1022// DeleteAPIKey deletes an existing API key 1023func DeleteAPIKey(keyID string, executor, ipAddress string) error { 1024 apiKey, err := provider.apiKeyExists(keyID) 1025 if err != nil { 1026 return err 1027 } 1028 err = provider.deleteAPIKey(&apiKey) 1029 if err == nil { 1030 executeAction(operationDelete, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, &apiKey) 1031 } 1032 return err 1033} 1034 1035// APIKeyExists returns the API key with the given ID if it exists 1036func APIKeyExists(keyID string) (APIKey, error) { 1037 if keyID == "" { 1038 return APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf("API key %#v does not exist", keyID)) 1039 } 1040 return provider.apiKeyExists(keyID) 1041} 1042 1043// HasAdmin returns true if the first admin has been created 1044// and so SFTPGo is ready to be used 1045func HasAdmin() bool { 1046 return atomic.LoadInt32(&isAdminCreated) > 0 1047} 1048 1049// AddAdmin adds a new SFTPGo admin 1050func AddAdmin(admin *Admin, executor, ipAddress string) error { 1051 admin.Filters.RecoveryCodes = nil 1052 admin.Filters.TOTPConfig = TOTPConfig{ 1053 Enabled: false, 1054 } 1055 err := provider.addAdmin(admin) 1056 if err == nil { 1057 atomic.StoreInt32(&isAdminCreated, 1) 1058 executeAction(operationAdd, executor, ipAddress, actionObjectAdmin, admin.Username, admin) 1059 } 1060 return err 1061} 1062 1063// UpdateAdmin updates an existing SFTPGo admin 1064func UpdateAdmin(admin *Admin, executor, ipAddress string) error { 1065 err := provider.updateAdmin(admin) 1066 if err == nil { 1067 executeAction(operationUpdate, executor, ipAddress, actionObjectAdmin, admin.Username, admin) 1068 } 1069 return err 1070} 1071 1072// DeleteAdmin deletes an existing SFTPGo admin 1073func DeleteAdmin(username, executor, ipAddress string) error { 1074 admin, err := provider.adminExists(username) 1075 if err != nil { 1076 return err 1077 } 1078 err = provider.deleteAdmin(&admin) 1079 if err == nil { 1080 executeAction(operationDelete, executor, ipAddress, actionObjectAdmin, admin.Username, &admin) 1081 } 1082 return err 1083} 1084 1085// AdminExists returns the admin with the given username if it exists 1086func AdminExists(username string) (Admin, error) { 1087 return provider.adminExists(username) 1088} 1089 1090// UserExists checks if the given SFTPGo username exists, returns an error if no match is found 1091func UserExists(username string) (User, error) { 1092 return provider.userExists(username) 1093} 1094 1095// AddUser adds a new SFTPGo user. 1096func AddUser(user *User, executor, ipAddress string) error { 1097 user.Filters.RecoveryCodes = nil 1098 user.Filters.TOTPConfig = sdk.TOTPConfig{ 1099 Enabled: false, 1100 } 1101 err := provider.addUser(user) 1102 if err == nil { 1103 executeAction(operationAdd, executor, ipAddress, actionObjectUser, user.Username, user) 1104 } 1105 return err 1106} 1107 1108// UpdateUser updates an existing SFTPGo user. 1109func UpdateUser(user *User, executor, ipAddress string) error { 1110 err := provider.updateUser(user) 1111 if err == nil { 1112 webDAVUsersCache.swap(user) 1113 cachedPasswords.Remove(user.Username) 1114 executeAction(operationUpdate, executor, ipAddress, actionObjectUser, user.Username, user) 1115 } 1116 return err 1117} 1118 1119// DeleteUser deletes an existing SFTPGo user. 1120func DeleteUser(username, executor, ipAddress string) error { 1121 user, err := provider.userExists(username) 1122 if err != nil { 1123 return err 1124 } 1125 err = provider.deleteUser(&user) 1126 if err == nil { 1127 RemoveCachedWebDAVUser(user.Username) 1128 delayedQuotaUpdater.resetUserQuota(username) 1129 cachedPasswords.Remove(username) 1130 executeAction(operationDelete, executor, ipAddress, actionObjectUser, user.Username, &user) 1131 } 1132 return err 1133} 1134 1135// ReloadConfig reloads provider configuration. 1136// Currently only implemented for memory provider, allows to reload the users 1137// from the configured file, if defined 1138func ReloadConfig() error { 1139 return provider.reloadConfig() 1140} 1141 1142// GetShares returns an array of shares respecting limit and offset 1143func GetShares(limit, offset int, order, username string) ([]Share, error) { 1144 return provider.getShares(limit, offset, order, username) 1145} 1146 1147// GetAPIKeys returns an array of API keys respecting limit and offset 1148func GetAPIKeys(limit, offset int, order string) ([]APIKey, error) { 1149 return provider.getAPIKeys(limit, offset, order) 1150} 1151 1152// GetAdmins returns an array of admins respecting limit and offset 1153func GetAdmins(limit, offset int, order string) ([]Admin, error) { 1154 return provider.getAdmins(limit, offset, order) 1155} 1156 1157// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty 1158func GetUsers(limit, offset int, order string) ([]User, error) { 1159 return provider.getUsers(limit, offset, order) 1160} 1161 1162// AddFolder adds a new virtual folder. 1163func AddFolder(folder *vfs.BaseVirtualFolder) error { 1164 return provider.addFolder(folder) 1165} 1166 1167// UpdateFolder updates the specified virtual folder 1168func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string, executor, ipAddress string) error { 1169 err := provider.updateFolder(folder) 1170 if err == nil { 1171 for _, user := range users { 1172 provider.setUpdatedAt(user) 1173 u, err := provider.userExists(user) 1174 if err == nil { 1175 webDAVUsersCache.swap(&u) 1176 executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u) 1177 } else { 1178 RemoveCachedWebDAVUser(user) 1179 } 1180 } 1181 } 1182 return err 1183} 1184 1185// DeleteFolder deletes an existing folder. 1186func DeleteFolder(folderName, executor, ipAddress string) error { 1187 folder, err := provider.getFolderByName(folderName) 1188 if err != nil { 1189 return err 1190 } 1191 err = provider.deleteFolder(&folder) 1192 if err == nil { 1193 for _, user := range folder.Users { 1194 provider.setUpdatedAt(user) 1195 u, err := provider.userExists(user) 1196 if err == nil { 1197 executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u) 1198 } 1199 RemoveCachedWebDAVUser(user) 1200 } 1201 delayedQuotaUpdater.resetFolderQuota(folderName) 1202 } 1203 return err 1204} 1205 1206// GetFolderByName returns the folder with the specified name if any 1207func GetFolderByName(name string) (vfs.BaseVirtualFolder, error) { 1208 return provider.getFolderByName(name) 1209} 1210 1211// GetFolders returns an array of folders respecting limit and offset 1212func GetFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) { 1213 return provider.getFolders(limit, offset, order) 1214} 1215 1216// DumpData returns all users and folders 1217func DumpData() (BackupData, error) { 1218 var data BackupData 1219 users, err := provider.dumpUsers() 1220 if err != nil { 1221 return data, err 1222 } 1223 folders, err := provider.dumpFolders() 1224 if err != nil { 1225 return data, err 1226 } 1227 admins, err := provider.dumpAdmins() 1228 if err != nil { 1229 return data, err 1230 } 1231 apiKeys, err := provider.dumpAPIKeys() 1232 if err != nil { 1233 return data, err 1234 } 1235 shares, err := provider.dumpShares() 1236 if err != nil { 1237 return data, err 1238 } 1239 data.Users = users 1240 data.Folders = folders 1241 data.Admins = admins 1242 data.APIKeys = apiKeys 1243 data.Shares = shares 1244 data.Version = DumpVersion 1245 return data, err 1246} 1247 1248// ParseDumpData tries to parse data as BackupData 1249func ParseDumpData(data []byte) (BackupData, error) { 1250 var dump BackupData 1251 err := json.Unmarshal(data, &dump) 1252 return dump, err 1253} 1254 1255// GetProviderStatus returns an error if the provider is not available 1256func GetProviderStatus() ProviderStatus { 1257 err := provider.checkAvailability() 1258 status := ProviderStatus{ 1259 Driver: config.Driver, 1260 } 1261 if err == nil { 1262 status.IsActive = true 1263 } else { 1264 status.IsActive = false 1265 status.Error = err.Error() 1266 } 1267 return status 1268} 1269 1270// Close releases all provider resources. 1271// This method is used in test cases. 1272// Closing an uninitialized provider is not supported 1273func Close() error { 1274 if availabilityTicker != nil { 1275 availabilityTicker.Stop() 1276 availabilityTickerDone <- true 1277 availabilityTicker = nil 1278 } 1279 if updateCachesTicker != nil { 1280 updateCachesTicker.Stop() 1281 updateCachesTickerDone <- true 1282 updateCachesTicker = nil 1283 } 1284 return provider.close() 1285} 1286 1287func createProvider(basePath string) error { 1288 var err error 1289 sqlPlaceholders = getSQLPlaceholders() 1290 if err = validateSQLTablesPrefix(); err != nil { 1291 return err 1292 } 1293 logSender = fmt.Sprintf("dataprovider_%v", config.Driver) 1294 1295 switch config.Driver { 1296 case SQLiteDataProviderName: 1297 return initializeSQLiteProvider(basePath) 1298 case PGSQLDataProviderName, CockroachDataProviderName: 1299 return initializePGSQLProvider() 1300 case MySQLDataProviderName: 1301 return initializeMySQLProvider() 1302 case BoltDataProviderName: 1303 return initializeBoltProvider(basePath) 1304 case MemoryDataProviderName: 1305 initializeMemoryProvider(basePath) 1306 return nil 1307 default: 1308 return fmt.Errorf("unsupported data provider: %v", config.Driver) 1309 } 1310} 1311 1312func buildUserHomeDir(user *User) { 1313 if user.HomeDir == "" { 1314 if config.UsersBaseDir != "" { 1315 user.HomeDir = filepath.Join(config.UsersBaseDir, user.Username) 1316 return 1317 } 1318 switch user.FsConfig.Provider { 1319 case sdk.SFTPFilesystemProvider, sdk.S3FilesystemProvider, sdk.AzureBlobFilesystemProvider, sdk.GCSFilesystemProvider: 1320 if tempPath != "" { 1321 user.HomeDir = filepath.Join(tempPath, user.Username) 1322 } else { 1323 user.HomeDir = filepath.Join(os.TempDir(), user.Username) 1324 } 1325 } 1326 } 1327} 1328 1329func isVirtualDirOverlapped(dir1, dir2 string, fullCheck bool) bool { 1330 if dir1 == dir2 { 1331 return true 1332 } 1333 if fullCheck { 1334 if len(dir1) > len(dir2) { 1335 if strings.HasPrefix(dir1, dir2+"/") { 1336 return true 1337 } 1338 } 1339 if len(dir2) > len(dir1) { 1340 if strings.HasPrefix(dir2, dir1+"/") { 1341 return true 1342 } 1343 } 1344 } 1345 return false 1346} 1347 1348func isMappedDirOverlapped(dir1, dir2 string, fullCheck bool) bool { 1349 if dir1 == dir2 { 1350 return true 1351 } 1352 if fullCheck { 1353 if len(dir1) > len(dir2) { 1354 if strings.HasPrefix(dir1, dir2+string(os.PathSeparator)) { 1355 return true 1356 } 1357 } 1358 if len(dir2) > len(dir1) { 1359 if strings.HasPrefix(dir2, dir1+string(os.PathSeparator)) { 1360 return true 1361 } 1362 } 1363 } 1364 return false 1365} 1366 1367func validateFolderQuotaLimits(folder vfs.VirtualFolder) error { 1368 if folder.QuotaSize < -1 { 1369 return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %#v", folder.QuotaSize, folder.MappedPath)) 1370 } 1371 if folder.QuotaFiles < -1 { 1372 return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %#v", folder.QuotaFiles, folder.MappedPath)) 1373 } 1374 if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) { 1375 return util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", 1376 folder.QuotaFiles, folder.QuotaSize)) 1377 } 1378 return nil 1379} 1380 1381func getVirtualFolderIfInvalid(folder *vfs.BaseVirtualFolder) *vfs.BaseVirtualFolder { 1382 if err := ValidateFolder(folder); err == nil { 1383 return folder 1384 } 1385 // we try to get the folder from the data provider if only the Name is populated 1386 if folder.MappedPath != "" { 1387 return folder 1388 } 1389 if folder.Name == "" { 1390 return folder 1391 } 1392 if folder.FsConfig.Provider != sdk.LocalFilesystemProvider { 1393 return folder 1394 } 1395 if f, err := GetFolderByName(folder.Name); err == nil { 1396 return &f 1397 } 1398 return folder 1399} 1400 1401func validateUserVirtualFolders(user *User) error { 1402 if len(user.VirtualFolders) == 0 { 1403 user.VirtualFolders = []vfs.VirtualFolder{} 1404 return nil 1405 } 1406 var virtualFolders []vfs.VirtualFolder 1407 mappedPaths := make(map[string]bool) 1408 virtualPaths := make(map[string]bool) 1409 for _, v := range user.VirtualFolders { 1410 cleanedVPath := filepath.ToSlash(path.Clean(v.VirtualPath)) 1411 if !path.IsAbs(cleanedVPath) || cleanedVPath == "/" { 1412 return util.NewValidationError(fmt.Sprintf("invalid virtual folder %#v", v.VirtualPath)) 1413 } 1414 if err := validateFolderQuotaLimits(v); err != nil { 1415 return err 1416 } 1417 folder := getVirtualFolderIfInvalid(&v.BaseVirtualFolder) 1418 if err := ValidateFolder(folder); err != nil { 1419 return err 1420 } 1421 cleanedMPath := folder.MappedPath 1422 if folder.IsLocalOrLocalCrypted() { 1423 if isMappedDirOverlapped(cleanedMPath, user.GetHomeDir(), true) { 1424 return util.NewValidationError(fmt.Sprintf("invalid mapped folder %#v cannot be inside or contain the user home dir %#v", 1425 folder.MappedPath, user.GetHomeDir())) 1426 } 1427 for mPath := range mappedPaths { 1428 if folder.IsLocalOrLocalCrypted() && isMappedDirOverlapped(mPath, cleanedMPath, false) { 1429 return util.NewValidationError(fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v", 1430 v.MappedPath, mPath)) 1431 } 1432 } 1433 mappedPaths[cleanedMPath] = true 1434 } 1435 for vPath := range virtualPaths { 1436 if isVirtualDirOverlapped(vPath, cleanedVPath, false) { 1437 return util.NewValidationError(fmt.Sprintf("invalid virtual folder %#v overlaps with virtual folder %#v", 1438 v.VirtualPath, vPath)) 1439 } 1440 } 1441 virtualPaths[cleanedVPath] = true 1442 virtualFolders = append(virtualFolders, vfs.VirtualFolder{ 1443 BaseVirtualFolder: *folder, 1444 VirtualPath: cleanedVPath, 1445 QuotaSize: v.QuotaSize, 1446 QuotaFiles: v.QuotaFiles, 1447 }) 1448 } 1449 user.VirtualFolders = virtualFolders 1450 return nil 1451} 1452 1453func validateUserTOTPConfig(c *sdk.TOTPConfig, username string) error { 1454 if !c.Enabled { 1455 c.ConfigName = "" 1456 c.Secret = kms.NewEmptySecret() 1457 c.Protocols = nil 1458 return nil 1459 } 1460 if c.ConfigName == "" { 1461 return util.NewValidationError("totp: config name is mandatory") 1462 } 1463 if !util.IsStringInSlice(c.ConfigName, mfa.GetAvailableTOTPConfigNames()) { 1464 return util.NewValidationError(fmt.Sprintf("totp: config name %#v not found", c.ConfigName)) 1465 } 1466 if c.Secret.IsEmpty() { 1467 return util.NewValidationError("totp: secret is mandatory") 1468 } 1469 if c.Secret.IsPlain() { 1470 c.Secret.SetAdditionalData(username) 1471 if err := c.Secret.Encrypt(); err != nil { 1472 return util.NewValidationError(fmt.Sprintf("totp: unable to encrypt secret: %v", err)) 1473 } 1474 } 1475 c.Protocols = util.RemoveDuplicates(c.Protocols) 1476 if len(c.Protocols) == 0 { 1477 return util.NewValidationError("totp: specify at least one protocol") 1478 } 1479 for _, protocol := range c.Protocols { 1480 if !util.IsStringInSlice(protocol, MFAProtocols) { 1481 return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %#v", protocol)) 1482 } 1483 } 1484 return nil 1485} 1486 1487func validateUserRecoveryCodes(user *User) error { 1488 for i := 0; i < len(user.Filters.RecoveryCodes); i++ { 1489 code := &user.Filters.RecoveryCodes[i] 1490 if code.Secret.IsEmpty() { 1491 return util.NewValidationError("mfa: recovery code cannot be empty") 1492 } 1493 if code.Secret.IsPlain() { 1494 code.Secret.SetAdditionalData(user.Username) 1495 if err := code.Secret.Encrypt(); err != nil { 1496 return util.NewValidationError(fmt.Sprintf("mfa: unable to encrypt recovery code: %v", err)) 1497 } 1498 } 1499 } 1500 return nil 1501} 1502 1503func validatePermissions(user *User) error { 1504 if len(user.Permissions) == 0 { 1505 return util.NewValidationError("please grant some permissions to this user") 1506 } 1507 permissions := make(map[string][]string) 1508 if _, ok := user.Permissions["/"]; !ok { 1509 return util.NewValidationError("permissions for the root dir \"/\" must be set") 1510 } 1511 for dir, perms := range user.Permissions { 1512 if len(perms) == 0 && dir == "/" { 1513 return util.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %#v", dir)) 1514 } 1515 if len(perms) > len(ValidPerms) { 1516 return util.NewValidationError("invalid permissions") 1517 } 1518 for _, p := range perms { 1519 if !util.IsStringInSlice(p, ValidPerms) { 1520 return util.NewValidationError(fmt.Sprintf("invalid permission: %#v", p)) 1521 } 1522 } 1523 cleanedDir := filepath.ToSlash(path.Clean(dir)) 1524 if cleanedDir != "/" { 1525 cleanedDir = strings.TrimSuffix(cleanedDir, "/") 1526 } 1527 if !path.IsAbs(cleanedDir) { 1528 return util.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %#v", dir)) 1529 } 1530 if dir != cleanedDir && cleanedDir == "/" { 1531 return util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"", dir)) 1532 } 1533 if util.IsStringInSlice(PermAny, perms) { 1534 permissions[cleanedDir] = []string{PermAny} 1535 } else { 1536 permissions[cleanedDir] = util.RemoveDuplicates(perms) 1537 } 1538 } 1539 user.Permissions = permissions 1540 return nil 1541} 1542 1543func validatePublicKeys(user *User) error { 1544 if len(user.PublicKeys) == 0 { 1545 user.PublicKeys = []string{} 1546 } 1547 var validatedKeys []string 1548 for i, k := range user.PublicKeys { 1549 if k == "" { 1550 continue 1551 } 1552 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) 1553 if err != nil { 1554 return util.NewValidationError(fmt.Sprintf("could not parse key nr. %d: %s", i+1, err)) 1555 } 1556 validatedKeys = append(validatedKeys, k) 1557 } 1558 user.PublicKeys = util.RemoveDuplicates(validatedKeys) 1559 return nil 1560} 1561 1562func validateFiltersPatternExtensions(user *User) error { 1563 if len(user.Filters.FilePatterns) == 0 { 1564 user.Filters.FilePatterns = []sdk.PatternsFilter{} 1565 return nil 1566 } 1567 filteredPaths := []string{} 1568 var filters []sdk.PatternsFilter 1569 for _, f := range user.Filters.FilePatterns { 1570 cleanedPath := filepath.ToSlash(path.Clean(f.Path)) 1571 if !path.IsAbs(cleanedPath) { 1572 return util.NewValidationError(fmt.Sprintf("invalid path %#v for file patterns filter", f.Path)) 1573 } 1574 if util.IsStringInSlice(cleanedPath, filteredPaths) { 1575 return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %#v", f.Path)) 1576 } 1577 if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 { 1578 return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %#v", f.Path)) 1579 } 1580 f.Path = cleanedPath 1581 allowed := make([]string, 0, len(f.AllowedPatterns)) 1582 denied := make([]string, 0, len(f.DeniedPatterns)) 1583 for _, pattern := range f.AllowedPatterns { 1584 _, err := path.Match(pattern, "abc") 1585 if err != nil { 1586 return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) 1587 } 1588 allowed = append(allowed, strings.ToLower(pattern)) 1589 } 1590 for _, pattern := range f.DeniedPatterns { 1591 _, err := path.Match(pattern, "abc") 1592 if err != nil { 1593 return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) 1594 } 1595 denied = append(denied, strings.ToLower(pattern)) 1596 } 1597 f.AllowedPatterns = allowed 1598 f.DeniedPatterns = denied 1599 filters = append(filters, f) 1600 filteredPaths = append(filteredPaths, cleanedPath) 1601 } 1602 user.Filters.FilePatterns = filters 1603 return nil 1604} 1605 1606func checkEmptyFiltersStruct(user *User) { 1607 if len(user.Filters.AllowedIP) == 0 { 1608 user.Filters.AllowedIP = []string{} 1609 } 1610 if len(user.Filters.DeniedIP) == 0 { 1611 user.Filters.DeniedIP = []string{} 1612 } 1613 if len(user.Filters.DeniedLoginMethods) == 0 { 1614 user.Filters.DeniedLoginMethods = []string{} 1615 } 1616 if len(user.Filters.DeniedProtocols) == 0 { 1617 user.Filters.DeniedProtocols = []string{} 1618 } 1619} 1620 1621func validateFilters(user *User) error { 1622 checkEmptyFiltersStruct(user) 1623 user.Filters.DeniedIP = util.RemoveDuplicates(user.Filters.DeniedIP) 1624 for _, IPMask := range user.Filters.DeniedIP { 1625 _, _, err := net.ParseCIDR(IPMask) 1626 if err != nil { 1627 return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v : %v", IPMask, err)) 1628 } 1629 } 1630 user.Filters.AllowedIP = util.RemoveDuplicates(user.Filters.AllowedIP) 1631 for _, IPMask := range user.Filters.AllowedIP { 1632 _, _, err := net.ParseCIDR(IPMask) 1633 if err != nil { 1634 return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err)) 1635 } 1636 } 1637 user.Filters.DeniedLoginMethods = util.RemoveDuplicates(user.Filters.DeniedLoginMethods) 1638 if len(user.Filters.DeniedLoginMethods) >= len(ValidLoginMethods) { 1639 return util.NewValidationError("invalid denied_login_methods") 1640 } 1641 for _, loginMethod := range user.Filters.DeniedLoginMethods { 1642 if !util.IsStringInSlice(loginMethod, ValidLoginMethods) { 1643 return util.NewValidationError(fmt.Sprintf("invalid login method: %#v", loginMethod)) 1644 } 1645 } 1646 user.Filters.DeniedProtocols = util.RemoveDuplicates(user.Filters.DeniedProtocols) 1647 if len(user.Filters.DeniedProtocols) >= len(ValidProtocols) { 1648 return util.NewValidationError("invalid denied_protocols") 1649 } 1650 for _, p := range user.Filters.DeniedProtocols { 1651 if !util.IsStringInSlice(p, ValidProtocols) { 1652 return util.NewValidationError(fmt.Sprintf("invalid protocol: %#v", p)) 1653 } 1654 } 1655 if user.Filters.TLSUsername != "" { 1656 if !util.IsStringInSlice(string(user.Filters.TLSUsername), validTLSUsernames) { 1657 return util.NewValidationError(fmt.Sprintf("invalid TLS username: %#v", user.Filters.TLSUsername)) 1658 } 1659 } 1660 user.Filters.WebClient = util.RemoveDuplicates(user.Filters.WebClient) 1661 for _, opts := range user.Filters.WebClient { 1662 if !util.IsStringInSlice(opts, sdk.WebClientOptions) { 1663 return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts)) 1664 } 1665 } 1666 return validateFiltersPatternExtensions(user) 1667} 1668 1669func saveGCSCredentials(fsConfig *vfs.Filesystem, helper vfs.ValidatorHelper) error { 1670 if fsConfig.Provider != sdk.GCSFilesystemProvider { 1671 return nil 1672 } 1673 if fsConfig.GCSConfig.Credentials.GetPayload() == "" { 1674 return nil 1675 } 1676 if config.PreferDatabaseCredentials { 1677 if fsConfig.GCSConfig.Credentials.IsPlain() { 1678 fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncryptionAdditionalData()) 1679 err := fsConfig.GCSConfig.Credentials.Encrypt() 1680 if err != nil { 1681 return err 1682 } 1683 } 1684 return nil 1685 } 1686 if fsConfig.GCSConfig.Credentials.IsPlain() { 1687 fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncryptionAdditionalData()) 1688 err := fsConfig.GCSConfig.Credentials.Encrypt() 1689 if err != nil { 1690 return util.NewValidationError(fmt.Sprintf("could not encrypt GCS credentials: %v", err)) 1691 } 1692 } 1693 creds, err := json.Marshal(fsConfig.GCSConfig.Credentials) 1694 if err != nil { 1695 return util.NewValidationError(fmt.Sprintf("could not marshal GCS credentials: %v", err)) 1696 } 1697 credentialsFilePath := helper.GetGCSCredentialsFilePath() 1698 err = os.MkdirAll(filepath.Dir(credentialsFilePath), 0700) 1699 if err != nil { 1700 return util.NewValidationError(fmt.Sprintf("could not create GCS credentials dir: %v", err)) 1701 } 1702 err = os.WriteFile(credentialsFilePath, creds, 0600) 1703 if err != nil { 1704 return util.NewValidationError(fmt.Sprintf("could not save GCS credentials: %v", err)) 1705 } 1706 fsConfig.GCSConfig.Credentials = kms.NewEmptySecret() 1707 return nil 1708} 1709 1710func validateBaseParams(user *User) error { 1711 if user.Username == "" { 1712 return util.NewValidationError("username is mandatory") 1713 } 1714 if user.Email != "" && !emailRegex.MatchString(user.Email) { 1715 return util.NewValidationError(fmt.Sprintf("email %#v is not valid", user.Email)) 1716 } 1717 if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(user.Username) { 1718 return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", 1719 user.Username)) 1720 } 1721 if user.HomeDir == "" { 1722 return util.NewValidationError("home_dir is mandatory") 1723 } 1724 if user.Password == "" && len(user.PublicKeys) == 0 { 1725 return util.NewValidationError("please set a password or at least a public_key") 1726 } 1727 if !filepath.IsAbs(user.HomeDir) { 1728 return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)) 1729 } 1730 return nil 1731} 1732 1733func createUserPasswordHash(user *User) error { 1734 if user.Password != "" && !user.IsPasswordHashed() { 1735 if config.PasswordValidation.Users.MinEntropy > 0 { 1736 if err := passwordvalidator.Validate(user.Password, config.PasswordValidation.Users.MinEntropy); err != nil { 1737 return util.NewValidationError(err.Error()) 1738 } 1739 } 1740 if config.PasswordHashing.Algo == HashingAlgoBcrypt { 1741 pwd, err := bcrypt.GenerateFromPassword([]byte(user.Password), config.PasswordHashing.BcryptOptions.Cost) 1742 if err != nil { 1743 return err 1744 } 1745 user.Password = string(pwd) 1746 } else { 1747 pwd, err := argon2id.CreateHash(user.Password, argon2Params) 1748 if err != nil { 1749 return err 1750 } 1751 user.Password = pwd 1752 } 1753 } 1754 return nil 1755} 1756 1757// ValidateFolder returns an error if the folder is not valid 1758// FIXME: this should be defined as Folder struct method 1759func ValidateFolder(folder *vfs.BaseVirtualFolder) error { 1760 folder.FsConfig.SetEmptySecretsIfNil() 1761 if folder.Name == "" { 1762 return util.NewValidationError("folder name is mandatory") 1763 } 1764 if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(folder.Name) { 1765 return util.NewValidationError(fmt.Sprintf("folder name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", 1766 folder.Name)) 1767 } 1768 if folder.FsConfig.Provider == sdk.LocalFilesystemProvider || folder.FsConfig.Provider == sdk.CryptedFilesystemProvider || 1769 folder.MappedPath != "" { 1770 cleanedMPath := filepath.Clean(folder.MappedPath) 1771 if !filepath.IsAbs(cleanedMPath) { 1772 return util.NewValidationError(fmt.Sprintf("invalid folder mapped path %#v", folder.MappedPath)) 1773 } 1774 folder.MappedPath = cleanedMPath 1775 } 1776 if folder.HasRedactedSecret() { 1777 return errors.New("cannot save a folder with a redacted secret") 1778 } 1779 if err := folder.FsConfig.Validate(folder); err != nil { 1780 return err 1781 } 1782 return saveGCSCredentials(&folder.FsConfig, folder) 1783} 1784 1785// ValidateUser returns an error if the user is not valid 1786// FIXME: this should be defined as User struct method 1787func ValidateUser(user *User) error { 1788 user.SetEmptySecretsIfNil() 1789 buildUserHomeDir(user) 1790 if err := validateBaseParams(user); err != nil { 1791 return err 1792 } 1793 if err := validatePermissions(user); err != nil { 1794 return err 1795 } 1796 if user.hasRedactedSecret() { 1797 return util.NewValidationError("cannot save a user with a redacted secret") 1798 } 1799 if err := validateUserTOTPConfig(&user.Filters.TOTPConfig, user.Username); err != nil { 1800 return err 1801 } 1802 if err := validateUserRecoveryCodes(user); err != nil { 1803 return err 1804 } 1805 if err := user.FsConfig.Validate(user); err != nil { 1806 return err 1807 } 1808 if err := validateUserVirtualFolders(user); err != nil { 1809 return err 1810 } 1811 if user.Status < 0 || user.Status > 1 { 1812 return util.NewValidationError(fmt.Sprintf("invalid user status: %v", user.Status)) 1813 } 1814 if err := createUserPasswordHash(user); err != nil { 1815 return err 1816 } 1817 if err := validatePublicKeys(user); err != nil { 1818 return err 1819 } 1820 if err := validateFilters(user); err != nil { 1821 return err 1822 } 1823 if user.Filters.TOTPConfig.Enabled && util.IsStringInSlice(sdk.WebClientMFADisabled, user.Filters.WebClient) { 1824 return util.NewValidationError("multi-factor authentication cannot be disabled for a user with an active configuration") 1825 } 1826 return saveGCSCredentials(&user.FsConfig, user) 1827} 1828 1829func isPasswordOK(user *User, password string) (bool, error) { 1830 if config.PasswordCaching { 1831 found, match := cachedPasswords.Check(user.Username, password) 1832 if found { 1833 return match, nil 1834 } 1835 } 1836 1837 match := false 1838 var err error 1839 if strings.HasPrefix(user.Password, bcryptPwdPrefix) { 1840 if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { 1841 return match, ErrInvalidCredentials 1842 } 1843 match = true 1844 } else if strings.HasPrefix(user.Password, argonPwdPrefix) { 1845 match, err = argon2id.ComparePasswordAndHash(password, user.Password) 1846 if err != nil { 1847 providerLog(logger.LevelWarn, "error comparing password with argon hash: %v", err) 1848 return match, err 1849 } 1850 } else if util.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) { 1851 match, err = comparePbkdf2PasswordAndHash(password, user.Password) 1852 if err != nil { 1853 return match, err 1854 } 1855 } else if util.IsStringPrefixInSlice(user.Password, unixPwdPrefixes) { 1856 match, err = compareUnixPasswordAndHash(user, password) 1857 if err != nil { 1858 return match, err 1859 } 1860 } 1861 if err == nil && match { 1862 cachedPasswords.Add(user.Username, password) 1863 } 1864 return match, err 1865} 1866 1867func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certificate) (User, error) { 1868 err := user.CheckLoginConditions() 1869 if err != nil { 1870 return *user, err 1871 } 1872 switch protocol { 1873 case protocolFTP, protocolWebDAV: 1874 if user.Filters.TLSUsername == sdk.TLSUsernameCN { 1875 if user.Username == tlsCert.Subject.CommonName { 1876 return *user, nil 1877 } 1878 return *user, fmt.Errorf("CN %#v does not match username %#v", tlsCert.Subject.CommonName, user.Username) 1879 } 1880 return *user, errors.New("TLS certificate is not valid") 1881 default: 1882 return *user, fmt.Errorf("certificate authentication is not supported for protocol %v", protocol) 1883 } 1884} 1885 1886func checkUserAndPass(user *User, password, ip, protocol string) (User, error) { 1887 err := user.CheckLoginConditions() 1888 if err != nil { 1889 return *user, err 1890 } 1891 password, err = checkUserPasscode(user, password, protocol) 1892 if err != nil { 1893 return *user, ErrInvalidCredentials 1894 } 1895 if user.Password == "" { 1896 return *user, errors.New("credentials cannot be null or empty") 1897 } 1898 if !user.Filters.Hooks.CheckPasswordDisabled { 1899 hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol) 1900 if err != nil { 1901 providerLog(logger.LevelDebug, "error executing check password hook for user %#v, ip %v, protocol %v: %v", 1902 user.Username, ip, protocol, err) 1903 return *user, errors.New("unable to check credentials") 1904 } 1905 switch hookResponse.Status { 1906 case -1: 1907 // no hook configured 1908 case 1: 1909 providerLog(logger.LevelDebug, "password accepted by check password hook for user %#v, ip %v, protocol %v", 1910 user.Username, ip, protocol) 1911 return *user, nil 1912 case 2: 1913 providerLog(logger.LevelDebug, "partial success from check password hook for user %#v, ip %v, protocol %v", 1914 user.Username, ip, protocol) 1915 password = hookResponse.ToVerify 1916 default: 1917 providerLog(logger.LevelDebug, "password rejected by check password hook for user %#v, ip %v, protocol %v, status: %v", 1918 user.Username, ip, protocol, hookResponse.Status) 1919 return *user, ErrInvalidCredentials 1920 } 1921 } 1922 1923 match, err := isPasswordOK(user, password) 1924 if !match { 1925 err = ErrInvalidCredentials 1926 } 1927 return *user, err 1928} 1929 1930func checkUserPasscode(user *User, password, protocol string) (string, error) { 1931 if user.Filters.TOTPConfig.Enabled { 1932 switch protocol { 1933 case protocolFTP: 1934 if util.IsStringInSlice(protocol, user.Filters.TOTPConfig.Protocols) { 1935 // the TOTP passcode has six digits 1936 pwdLen := len(password) 1937 if pwdLen < 7 { 1938 providerLog(logger.LevelDebug, "password len %v is too short to contain a passcode, user %#v, protocol %v", 1939 pwdLen, user.Username, protocol) 1940 return "", util.NewValidationError("password too short, cannot contain the passcode") 1941 } 1942 err := user.Filters.TOTPConfig.Secret.TryDecrypt() 1943 if err != nil { 1944 providerLog(logger.LevelWarn, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v", 1945 user.Username, protocol, err) 1946 return "", err 1947 } 1948 pwd := password[0:(pwdLen - 6)] 1949 passcode := password[(pwdLen - 6):] 1950 match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode, 1951 user.Filters.TOTPConfig.Secret.GetPayload()) 1952 if !match || err != nil { 1953 providerLog(logger.LevelWarn, "invalid passcode for user %#v, protocol %v, err: %v", 1954 user.Username, protocol, err) 1955 return "", util.NewValidationError("invalid passcode") 1956 } 1957 return pwd, nil 1958 } 1959 } 1960 } 1961 return password, nil 1962} 1963 1964func checkUserAndPubKey(user *User, pubKey []byte) (User, string, error) { 1965 err := user.CheckLoginConditions() 1966 if err != nil { 1967 return *user, "", err 1968 } 1969 if len(user.PublicKeys) == 0 { 1970 return *user, "", ErrInvalidCredentials 1971 } 1972 for i, k := range user.PublicKeys { 1973 storedPubKey, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) 1974 if err != nil { 1975 providerLog(logger.LevelWarn, "error parsing stored public key %d for user %v: %v", i, user.Username, err) 1976 return *user, "", err 1977 } 1978 if bytes.Equal(storedPubKey.Marshal(), pubKey) { 1979 certInfo := "" 1980 cert, ok := storedPubKey.(*ssh.Certificate) 1981 if ok { 1982 certInfo = fmt.Sprintf(" %v ID: %v Serial: %v CA: %v", cert.Type(), cert.KeyId, cert.Serial, 1983 ssh.FingerprintSHA256(cert.SignatureKey)) 1984 } 1985 return *user, fmt.Sprintf("%v:%v%v", ssh.FingerprintSHA256(storedPubKey), comment, certInfo), nil 1986 } 1987 } 1988 return *user, "", ErrInvalidCredentials 1989} 1990 1991func compareUnixPasswordAndHash(user *User, password string) (bool, error) { 1992 var crypter crypt.Crypter 1993 if strings.HasPrefix(user.Password, sha512cryptPwdPrefix) { 1994 crypter = sha512_crypt.New() 1995 } else if strings.HasPrefix(user.Password, md5cryptPwdPrefix) { 1996 crypter = md5_crypt.New() 1997 } else if strings.HasPrefix(user.Password, md5cryptApr1PwdPrefix) { 1998 crypter = apr1_crypt.New() 1999 } else { 2000 return false, errors.New("unix crypt: invalid or unsupported hash format") 2001 } 2002 if err := crypter.Verify(user.Password, []byte(password)); err != nil { 2003 return false, err 2004 } 2005 return true, nil 2006} 2007 2008func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error) { 2009 vals := strings.Split(hashedPassword, "$") 2010 if len(vals) != 5 { 2011 return false, fmt.Errorf("pbkdf2: hash is not in the correct format") 2012 } 2013 iterations, err := strconv.Atoi(vals[2]) 2014 if err != nil { 2015 return false, err 2016 } 2017 expected, err := base64.StdEncoding.DecodeString(vals[4]) 2018 if err != nil { 2019 return false, err 2020 } 2021 var salt []byte 2022 if util.IsStringPrefixInSlice(hashedPassword, pbkdfPwdB64SaltPrefixes) { 2023 salt, err = base64.StdEncoding.DecodeString(vals[3]) 2024 if err != nil { 2025 return false, err 2026 } 2027 } else { 2028 salt = []byte(vals[3]) 2029 } 2030 var hashFunc func() hash.Hash 2031 if strings.HasPrefix(hashedPassword, pbkdf2SHA256Prefix) || strings.HasPrefix(hashedPassword, pbkdf2SHA256B64SaltPrefix) { 2032 hashFunc = sha256.New 2033 } else if strings.HasPrefix(hashedPassword, pbkdf2SHA512Prefix) { 2034 hashFunc = sha512.New 2035 } else if strings.HasPrefix(hashedPassword, pbkdf2SHA1Prefix) { 2036 hashFunc = sha1.New 2037 } else { 2038 return false, fmt.Errorf("pbkdf2: invalid or unsupported hash format %v", vals[1]) 2039 } 2040 df := pbkdf2.Key([]byte(password), salt, iterations, len(expected), hashFunc) 2041 return subtle.ConstantTimeCompare(df, expected) == 1, nil 2042} 2043 2044func addCredentialsToUser(user *User) error { 2045 if err := addFolderCredentialsToUser(user); err != nil { 2046 return err 2047 } 2048 if user.FsConfig.Provider != sdk.GCSFilesystemProvider { 2049 return nil 2050 } 2051 if user.FsConfig.GCSConfig.AutomaticCredentials > 0 { 2052 return nil 2053 } 2054 2055 // Don't read from file if credentials have already been set 2056 if user.FsConfig.GCSConfig.Credentials.IsValid() { 2057 return nil 2058 } 2059 2060 cred, err := os.ReadFile(user.GetGCSCredentialsFilePath()) 2061 if err != nil { 2062 return err 2063 } 2064 return json.Unmarshal(cred, &user.FsConfig.GCSConfig.Credentials) 2065} 2066 2067func addFolderCredentialsToUser(user *User) error { 2068 for idx := range user.VirtualFolders { 2069 f := &user.VirtualFolders[idx] 2070 if f.FsConfig.Provider != sdk.GCSFilesystemProvider { 2071 continue 2072 } 2073 if f.FsConfig.GCSConfig.AutomaticCredentials > 0 { 2074 continue 2075 } 2076 // Don't read from file if credentials have already been set 2077 if f.FsConfig.GCSConfig.Credentials.IsValid() { 2078 continue 2079 } 2080 cred, err := os.ReadFile(f.GetGCSCredentialsFilePath()) 2081 if err != nil { 2082 return err 2083 } 2084 err = json.Unmarshal(cred, f.FsConfig.GCSConfig.Credentials) 2085 if err != nil { 2086 return err 2087 } 2088 } 2089 return nil 2090} 2091 2092func getSSLMode() string { 2093 if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName { 2094 if config.SSLMode == 0 { 2095 return "disable" 2096 } else if config.SSLMode == 1 { 2097 return "require" 2098 } else if config.SSLMode == 2 { 2099 return "verify-ca" 2100 } else if config.SSLMode == 3 { 2101 return "verify-full" 2102 } 2103 } else if config.Driver == MySQLDataProviderName { 2104 if config.SSLMode == 0 { 2105 return "false" 2106 } else if config.SSLMode == 1 { 2107 return "true" 2108 } else if config.SSLMode == 2 { 2109 return "skip-verify" 2110 } else if config.SSLMode == 3 { 2111 return "preferred" 2112 } 2113 } 2114 return "" 2115} 2116 2117func checkCacheUpdates() { 2118 providerLog(logger.LevelDebug, "start caches check, update time %v", util.GetTimeFromMsecSinceEpoch(lastCachesUpdate)) 2119 checkTime := util.GetTimeAsMsSinceEpoch(time.Now()) 2120 users, err := provider.getRecentlyUpdatedUsers(lastCachesUpdate) 2121 if err != nil { 2122 providerLog(logger.LevelWarn, "unable to get recently updated users: %v", err) 2123 return 2124 } 2125 for _, user := range users { 2126 providerLog(logger.LevelDebug, "invalidate caches for user %#v", user.Username) 2127 webDAVUsersCache.swap(&user) 2128 cachedPasswords.Remove(user.Username) 2129 } 2130 2131 lastCachesUpdate = checkTime 2132 providerLog(logger.LevelDebug, "end caches check, new update time %v", util.GetTimeFromMsecSinceEpoch(lastCachesUpdate)) 2133} 2134 2135func startUpdateCachesTimer() { 2136 if config.IsShared == 0 { 2137 return 2138 } 2139 if !util.IsStringInSlice(config.Driver, sharedProviders) { 2140 providerLog(logger.LevelWarn, "update caches not supported for provider %v", config.Driver) 2141 return 2142 } 2143 lastCachesUpdate = util.GetTimeAsMsSinceEpoch(time.Now()) 2144 providerLog(logger.LevelDebug, "update caches check started for provider %v", config.Driver) 2145 updateCachesTicker = time.NewTicker(1 * time.Minute) 2146 updateCachesTickerDone = make(chan bool) 2147 2148 go func() { 2149 for { 2150 select { 2151 case <-updateCachesTickerDone: 2152 return 2153 case <-updateCachesTicker.C: 2154 checkCacheUpdates() 2155 } 2156 } 2157 }() 2158} 2159 2160func startAvailabilityTimer() { 2161 availabilityTicker = time.NewTicker(30 * time.Second) 2162 availabilityTickerDone = make(chan bool) 2163 checkDataprovider() 2164 go func() { 2165 for { 2166 select { 2167 case <-availabilityTickerDone: 2168 return 2169 case <-availabilityTicker.C: 2170 checkDataprovider() 2171 } 2172 } 2173 }() 2174} 2175 2176func checkDataprovider() { 2177 err := provider.checkAvailability() 2178 if err != nil { 2179 providerLog(logger.LevelWarn, "check availability error: %v", err) 2180 } 2181 metric.UpdateDataProviderAvailability(err) 2182} 2183 2184func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) { 2185 if isFinished { 2186 return 2187 } 2188 providerLog(logger.LevelInfo, "kill interactive auth program after an unexpected error") 2189 err := cmd.Process.Kill() 2190 if err != nil { 2191 providerLog(logger.LevelDebug, "error killing interactive auth program: %v", err) 2192 } 2193} 2194 2195func sendKeyboardAuthHTTPReq(url string, request *plugin.KeyboardAuthRequest) (*plugin.KeyboardAuthResponse, error) { 2196 reqAsJSON, err := json.Marshal(request) 2197 if err != nil { 2198 providerLog(logger.LevelWarn, "error serializing keyboard interactive auth request: %v", err) 2199 return nil, err 2200 } 2201 resp, err := httpclient.Post(url, "application/json", bytes.NewBuffer(reqAsJSON)) 2202 if err != nil { 2203 providerLog(logger.LevelWarn, "error getting keyboard interactive auth hook HTTP response: %v", err) 2204 return nil, err 2205 } 2206 defer resp.Body.Close() 2207 if resp.StatusCode != http.StatusOK { 2208 return nil, fmt.Errorf("wrong keyboard interactive auth http status code: %v, expected 200", resp.StatusCode) 2209 } 2210 var response plugin.KeyboardAuthResponse 2211 err = render.DecodeJSON(resp.Body, &response) 2212 return &response, err 2213} 2214 2215func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) { 2216 answers, err := client(user.Username, "", []string{"Password: "}, []bool{false}) 2217 if err != nil { 2218 return 0, err 2219 } 2220 if len(answers) != 1 { 2221 return 0, fmt.Errorf("unexpected number of answers: %v", len(answers)) 2222 } 2223 _, err = checkUserAndPass(user, answers[0], ip, protocol) 2224 if err != nil { 2225 return 0, err 2226 } 2227 if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(protocolSSH, user.Filters.TOTPConfig.Protocols) { 2228 return 1, nil 2229 } 2230 err = user.Filters.TOTPConfig.Secret.TryDecrypt() 2231 if err != nil { 2232 providerLog(logger.LevelWarn, "unable to decrypt TOTP secret for user %#v, protocol %v, err: %v", 2233 user.Username, protocol, err) 2234 return 0, err 2235 } 2236 answers, err = client(user.Username, "", []string{"Authentication code: "}, []bool{false}) 2237 if err != nil { 2238 return 0, err 2239 } 2240 if len(answers) != 1 { 2241 return 0, fmt.Errorf("unexpected number of answers: %v", len(answers)) 2242 } 2243 match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0], 2244 user.Filters.TOTPConfig.Secret.GetPayload()) 2245 if !match || err != nil { 2246 providerLog(logger.LevelWarn, "invalid passcode for user %#v, protocol %v, err: %v", 2247 user.Username, protocol, err) 2248 return 0, util.NewValidationError("invalid passcode") 2249 } 2250 return 1, nil 2251} 2252 2253func executeKeyboardInteractivePlugin(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) { 2254 authResult := 0 2255 requestID := xid.New().String() 2256 authStep := 1 2257 req := &plugin.KeyboardAuthRequest{ 2258 Username: user.Username, 2259 IP: ip, 2260 Password: user.Password, 2261 RequestID: requestID, 2262 Step: authStep, 2263 } 2264 var response *plugin.KeyboardAuthResponse 2265 var err error 2266 for { 2267 response, err = plugin.Handler.ExecuteKeyboardInteractiveStep(req) 2268 if err != nil { 2269 return authResult, err 2270 } 2271 if response.AuthResult != 0 { 2272 return response.AuthResult, err 2273 } 2274 if err = response.Validate(); err != nil { 2275 providerLog(logger.LevelInfo, "invalid response from keyboard interactive plugin: %v", err) 2276 return authResult, err 2277 } 2278 answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol) 2279 if err != nil { 2280 return authResult, err 2281 } 2282 authStep++ 2283 req = &plugin.KeyboardAuthRequest{ 2284 RequestID: requestID, 2285 Step: authStep, 2286 Username: user.Username, 2287 Password: user.Password, 2288 Answers: answers, 2289 Questions: response.Questions, 2290 } 2291 } 2292} 2293 2294func executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) { 2295 authResult := 0 2296 requestID := xid.New().String() 2297 authStep := 1 2298 req := &plugin.KeyboardAuthRequest{ 2299 Username: user.Username, 2300 IP: ip, 2301 Password: user.Password, 2302 RequestID: requestID, 2303 Step: authStep, 2304 } 2305 var response *plugin.KeyboardAuthResponse 2306 var err error 2307 for { 2308 response, err = sendKeyboardAuthHTTPReq(authHook, req) 2309 if err != nil { 2310 return authResult, err 2311 } 2312 if response.AuthResult != 0 { 2313 return response.AuthResult, err 2314 } 2315 if err = response.Validate(); err != nil { 2316 providerLog(logger.LevelInfo, "invalid response from keyboard interactive http hook: %v", err) 2317 return authResult, err 2318 } 2319 answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol) 2320 if err != nil { 2321 return authResult, err 2322 } 2323 authStep++ 2324 req = &plugin.KeyboardAuthRequest{ 2325 RequestID: requestID, 2326 Step: authStep, 2327 Username: user.Username, 2328 Password: user.Password, 2329 Answers: answers, 2330 Questions: response.Questions, 2331 } 2332 } 2333} 2334 2335func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse, 2336 user *User, ip, protocol string, 2337) ([]string, error) { 2338 questions := response.Questions 2339 answers, err := client(user.Username, response.Instruction, questions, response.Echos) 2340 if err != nil { 2341 providerLog(logger.LevelInfo, "error getting interactive auth client response: %v", err) 2342 return answers, err 2343 } 2344 if len(answers) != len(questions) { 2345 err = fmt.Errorf("client answers does not match questions, expected: %v actual: %v", questions, answers) 2346 providerLog(logger.LevelInfo, "keyboard interactive auth error: %v", err) 2347 return answers, err 2348 } 2349 if len(answers) == 1 && response.CheckPwd > 0 { 2350 _, err = checkUserAndPass(user, answers[0], ip, protocol) 2351 providerLog(logger.LevelInfo, "interactive auth hook requested password validation for user %#v, validation error: %v", 2352 user.Username, err) 2353 if err != nil { 2354 return answers, err 2355 } 2356 answers[0] = "OK" 2357 } 2358 return answers, err 2359} 2360 2361func handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse, 2362 user *User, stdin io.WriteCloser, ip, protocol string, 2363) error { 2364 answers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol) 2365 if err != nil { 2366 return err 2367 } 2368 for _, answer := range answers { 2369 if runtime.GOOS == "windows" { 2370 answer += "\r" 2371 } 2372 answer += "\n" 2373 _, err = stdin.Write([]byte(answer)) 2374 if err != nil { 2375 providerLog(logger.LevelError, "unable to write client answer to keyboard interactive program: %v", err) 2376 return err 2377 } 2378 } 2379 return nil 2380} 2381 2382func executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) { 2383 authResult := 0 2384 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 2385 defer cancel() 2386 cmd := exec.CommandContext(ctx, authHook) 2387 cmd.Env = append(os.Environ(), 2388 fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", user.Username), 2389 fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), 2390 fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", user.Password)) 2391 stdout, err := cmd.StdoutPipe() 2392 if err != nil { 2393 return authResult, err 2394 } 2395 stdin, err := cmd.StdinPipe() 2396 if err != nil { 2397 return authResult, err 2398 } 2399 err = cmd.Start() 2400 if err != nil { 2401 return authResult, err 2402 } 2403 var once sync.Once 2404 scanner := bufio.NewScanner(stdout) 2405 for scanner.Scan() { 2406 var response plugin.KeyboardAuthResponse 2407 err = json.Unmarshal(scanner.Bytes(), &response) 2408 if err != nil { 2409 providerLog(logger.LevelInfo, "interactive auth error parsing response: %v", err) 2410 once.Do(func() { terminateInteractiveAuthProgram(cmd, false) }) 2411 break 2412 } 2413 if response.AuthResult != 0 { 2414 authResult = response.AuthResult 2415 break 2416 } 2417 if err = response.Validate(); err != nil { 2418 providerLog(logger.LevelInfo, "invalid response from keyboard interactive program: %v", err) 2419 once.Do(func() { terminateInteractiveAuthProgram(cmd, false) }) 2420 break 2421 } 2422 go func() { 2423 err := handleProgramInteractiveQuestions(client, &response, user, stdin, ip, protocol) 2424 if err != nil { 2425 once.Do(func() { terminateInteractiveAuthProgram(cmd, false) }) 2426 } 2427 }() 2428 } 2429 stdin.Close() 2430 once.Do(func() { terminateInteractiveAuthProgram(cmd, true) }) 2431 go func() { 2432 _, err := cmd.Process.Wait() 2433 if err != nil { 2434 providerLog(logger.LevelWarn, "error waiting for #%v process to exit: %v", authHook, err) 2435 } 2436 }() 2437 2438 return authResult, err 2439} 2440 2441func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) { 2442 var authResult int 2443 var err error 2444 if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) { 2445 authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol) 2446 } else if authHook != "" { 2447 if strings.HasPrefix(authHook, "http") { 2448 authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol) 2449 } else { 2450 authResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol) 2451 } 2452 } else { 2453 authResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol) 2454 } 2455 if err != nil { 2456 return *user, err 2457 } 2458 if authResult != 1 { 2459 return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult) 2460 } 2461 err = user.CheckLoginConditions() 2462 if err != nil { 2463 return *user, err 2464 } 2465 return *user, nil 2466} 2467 2468func isCheckPasswordHookDefined(protocol string) bool { 2469 if config.CheckPasswordHook == "" { 2470 return false 2471 } 2472 if config.CheckPasswordScope == 0 { 2473 return true 2474 } 2475 switch protocol { 2476 case protocolSSH: 2477 return config.CheckPasswordScope&1 != 0 2478 case protocolFTP: 2479 return config.CheckPasswordScope&2 != 0 2480 case protocolWebDAV: 2481 return config.CheckPasswordScope&4 != 0 2482 default: 2483 return false 2484 } 2485} 2486 2487func getPasswordHookResponse(username, password, ip, protocol string) ([]byte, error) { 2488 if strings.HasPrefix(config.CheckPasswordHook, "http") { 2489 var result []byte 2490 req := checkPasswordRequest{ 2491 Username: username, 2492 Password: password, 2493 IP: ip, 2494 Protocol: protocol, 2495 } 2496 reqAsJSON, err := json.Marshal(req) 2497 if err != nil { 2498 return result, err 2499 } 2500 resp, err := httpclient.Post(config.CheckPasswordHook, "application/json", bytes.NewBuffer(reqAsJSON)) 2501 if err != nil { 2502 providerLog(logger.LevelWarn, "error getting check password hook response: %v", err) 2503 return result, err 2504 } 2505 defer resp.Body.Close() 2506 if resp.StatusCode != http.StatusOK { 2507 return result, fmt.Errorf("wrong http status code from chek password hook: %v, expected 200", resp.StatusCode) 2508 } 2509 return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize)) 2510 } 2511 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 2512 defer cancel() 2513 cmd := exec.CommandContext(ctx, config.CheckPasswordHook) 2514 cmd.Env = append(os.Environ(), 2515 fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username), 2516 fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password), 2517 fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), 2518 fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%v", protocol), 2519 ) 2520 return cmd.Output() 2521} 2522 2523func executeCheckPasswordHook(username, password, ip, protocol string) (checkPasswordResponse, error) { 2524 var response checkPasswordResponse 2525 2526 if !isCheckPasswordHookDefined(protocol) { 2527 response.Status = -1 2528 return response, nil 2529 } 2530 2531 startTime := time.Now() 2532 out, err := getPasswordHookResponse(username, password, ip, protocol) 2533 providerLog(logger.LevelDebug, "check password hook executed, error: %v, elapsed: %v", err, time.Since(startTime)) 2534 if err != nil { 2535 return response, err 2536 } 2537 err = json.Unmarshal(out, &response) 2538 return response, err 2539} 2540 2541func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte) ([]byte, error) { 2542 if strings.HasPrefix(config.PreLoginHook, "http") { 2543 var url *url.URL 2544 var result []byte 2545 url, err := url.Parse(config.PreLoginHook) 2546 if err != nil { 2547 providerLog(logger.LevelWarn, "invalid url for pre-login hook %#v, error: %v", config.PreLoginHook, err) 2548 return result, err 2549 } 2550 q := url.Query() 2551 q.Add("login_method", loginMethod) 2552 q.Add("ip", ip) 2553 q.Add("protocol", protocol) 2554 url.RawQuery = q.Encode() 2555 2556 resp, err := httpclient.Post(url.String(), "application/json", bytes.NewBuffer(userAsJSON)) 2557 if err != nil { 2558 providerLog(logger.LevelWarn, "error getting pre-login hook response: %v", err) 2559 return result, err 2560 } 2561 defer resp.Body.Close() 2562 if resp.StatusCode == http.StatusNoContent { 2563 return result, nil 2564 } 2565 if resp.StatusCode != http.StatusOK { 2566 return result, fmt.Errorf("wrong pre-login hook http status code: %v, expected 200", resp.StatusCode) 2567 } 2568 return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize)) 2569 } 2570 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 2571 defer cancel() 2572 cmd := exec.CommandContext(ctx, config.PreLoginHook) 2573 cmd.Env = append(os.Environ(), 2574 fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)), 2575 fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod), 2576 fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip), 2577 fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", protocol), 2578 ) 2579 return cmd.Output() 2580} 2581 2582func executePreLoginHook(username, loginMethod, ip, protocol string) (User, error) { 2583 u, userAsJSON, err := getUserAndJSONForHook(username) 2584 if err != nil { 2585 return u, err 2586 } 2587 if u.Filters.Hooks.PreLoginDisabled { 2588 return u, nil 2589 } 2590 startTime := time.Now() 2591 out, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON) 2592 if err != nil { 2593 return u, fmt.Errorf("pre-login hook error: %v, username %#v, ip %v, protocol %v elapsed %v", 2594 err, username, ip, protocol, time.Since(startTime)) 2595 } 2596 providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %v", time.Since(startTime)) 2597 if util.IsByteArrayEmpty(out) { 2598 providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %#v id: %v", 2599 username, u.ID) 2600 if u.ID == 0 { 2601 return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) 2602 } 2603 return u, nil 2604 } 2605 2606 userID := u.ID 2607 userPwd := u.Password 2608 userUsedQuotaSize := u.UsedQuotaSize 2609 userUsedQuotaFiles := u.UsedQuotaFiles 2610 userLastQuotaUpdate := u.LastQuotaUpdate 2611 userLastLogin := u.LastLogin 2612 userCreatedAt := u.CreatedAt 2613 err = json.Unmarshal(out, &u) 2614 if err != nil { 2615 return u, fmt.Errorf("invalid pre-login hook response %#v, error: %v", string(out), err) 2616 } 2617 u.ID = userID 2618 u.UsedQuotaSize = userUsedQuotaSize 2619 u.UsedQuotaFiles = userUsedQuotaFiles 2620 u.LastQuotaUpdate = userLastQuotaUpdate 2621 u.LastLogin = userLastLogin 2622 u.CreatedAt = userCreatedAt 2623 if userID == 0 { 2624 err = provider.addUser(&u) 2625 } else { 2626 u.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) 2627 err = provider.updateUser(&u) 2628 if err == nil { 2629 webDAVUsersCache.swap(&u) 2630 if u.Password != userPwd { 2631 cachedPasswords.Remove(username) 2632 } 2633 } 2634 } 2635 if err != nil { 2636 return u, err 2637 } 2638 providerLog(logger.LevelDebug, "user %#v added/updated from pre-login hook response, id: %v", username, userID) 2639 if userID == 0 { 2640 return provider.userExists(username) 2641 } 2642 return u, nil 2643} 2644 2645// ExecutePostLoginHook executes the post login hook if defined 2646func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err error) { 2647 if config.PostLoginHook == "" { 2648 return 2649 } 2650 if config.PostLoginScope == 1 && err == nil { 2651 return 2652 } 2653 if config.PostLoginScope == 2 && err != nil { 2654 return 2655 } 2656 2657 go func() { 2658 status := "0" 2659 if err == nil { 2660 status = "1" 2661 } 2662 2663 user.PrepareForRendering() 2664 userAsJSON, err := json.Marshal(user) 2665 if err != nil { 2666 providerLog(logger.LevelWarn, "error serializing user in post login hook: %v", err) 2667 return 2668 } 2669 if strings.HasPrefix(config.PostLoginHook, "http") { 2670 var url *url.URL 2671 url, err := url.Parse(config.PostLoginHook) 2672 if err != nil { 2673 providerLog(logger.LevelDebug, "Invalid post-login hook %#v", config.PostLoginHook) 2674 return 2675 } 2676 q := url.Query() 2677 q.Add("login_method", loginMethod) 2678 q.Add("ip", ip) 2679 q.Add("protocol", protocol) 2680 q.Add("status", status) 2681 url.RawQuery = q.Encode() 2682 2683 startTime := time.Now() 2684 respCode := 0 2685 resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(userAsJSON)) 2686 if err == nil { 2687 respCode = resp.StatusCode 2688 resp.Body.Close() 2689 } 2690 providerLog(logger.LevelDebug, "post login hook executed for user %#v, ip %v, protocol %v, response code: %v, elapsed: %v err: %v", 2691 user.Username, ip, protocol, respCode, time.Since(startTime), err) 2692 return 2693 } 2694 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) 2695 defer cancel() 2696 cmd := exec.CommandContext(ctx, config.PostLoginHook) 2697 cmd.Env = append(os.Environ(), 2698 fmt.Sprintf("SFTPGO_LOGIND_USER=%v", string(userAsJSON)), 2699 fmt.Sprintf("SFTPGO_LOGIND_IP=%v", ip), 2700 fmt.Sprintf("SFTPGO_LOGIND_METHOD=%v", loginMethod), 2701 fmt.Sprintf("SFTPGO_LOGIND_STATUS=%v", status), 2702 fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%v", protocol)) 2703 startTime := time.Now() 2704 err = cmd.Run() 2705 providerLog(logger.LevelDebug, "post login hook executed for user %#v, ip %v, protocol %v, elapsed %v err: %v", 2706 user.Username, ip, protocol, time.Since(startTime), err) 2707 }() 2708} 2709 2710func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol string, cert *x509.Certificate, userAsJSON []byte) ([]byte, error) { 2711 var tlsCert string 2712 if cert != nil { 2713 var err error 2714 tlsCert, err = util.EncodeTLSCertToPem(cert) 2715 if err != nil { 2716 return nil, err 2717 } 2718 } 2719 if strings.HasPrefix(config.ExternalAuthHook, "http") { 2720 var result []byte 2721 authRequest := make(map[string]string) 2722 authRequest["username"] = username 2723 authRequest["ip"] = ip 2724 authRequest["password"] = password 2725 authRequest["public_key"] = pkey 2726 authRequest["protocol"] = protocol 2727 authRequest["keyboard_interactive"] = keyboardInteractive 2728 authRequest["tls_cert"] = tlsCert 2729 if len(userAsJSON) > 0 { 2730 authRequest["user"] = string(userAsJSON) 2731 } 2732 authRequestAsJSON, err := json.Marshal(authRequest) 2733 if err != nil { 2734 providerLog(logger.LevelWarn, "error serializing external auth request: %v", err) 2735 return result, err 2736 } 2737 resp, err := httpclient.Post(config.ExternalAuthHook, "application/json", bytes.NewBuffer(authRequestAsJSON)) 2738 if err != nil { 2739 providerLog(logger.LevelWarn, "error getting external auth hook HTTP response: %v", err) 2740 return result, err 2741 } 2742 defer resp.Body.Close() 2743 providerLog(logger.LevelDebug, "external auth hook executed, response code: %v", resp.StatusCode) 2744 if resp.StatusCode != http.StatusOK { 2745 return result, fmt.Errorf("wrong external auth http status code: %v, expected 200", resp.StatusCode) 2746 } 2747 2748 return io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize)) 2749 } 2750 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 2751 defer cancel() 2752 cmd := exec.CommandContext(ctx, config.ExternalAuthHook) 2753 cmd.Env = append(os.Environ(), 2754 fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%v", username), 2755 fmt.Sprintf("SFTPGO_AUTHD_USER=%v", string(userAsJSON)), 2756 fmt.Sprintf("SFTPGO_AUTHD_IP=%v", ip), 2757 fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%v", password), 2758 fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%v", pkey), 2759 fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%v", protocol), 2760 fmt.Sprintf("SFTPGO_AUTHD_TLS_CERT=%v", strings.ReplaceAll(tlsCert, "\n", "\\n")), 2761 fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive)) 2762 return cmd.Output() 2763} 2764 2765func updateUserFromExtAuthResponse(user *User, password, pkey string) { 2766 if password != "" { 2767 user.Password = password 2768 } 2769 if pkey != "" && !util.IsStringPrefixInSlice(pkey, user.PublicKeys) { 2770 user.PublicKeys = append(user.PublicKeys, pkey) 2771 } 2772} 2773 2774func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string, tlsCert *x509.Certificate) (User, error) { 2775 var user User 2776 2777 u, userAsJSON, err := getUserAndJSONForHook(username) 2778 if err != nil { 2779 return user, err 2780 } 2781 2782 if u.Filters.Hooks.ExternalAuthDisabled { 2783 return u, nil 2784 } 2785 2786 pkey, err := util.GetSSHPublicKeyAsString(pubKey) 2787 if err != nil { 2788 return user, err 2789 } 2790 2791 startTime := time.Now() 2792 out, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, userAsJSON) 2793 if err != nil { 2794 return user, fmt.Errorf("external auth error for user %#v: %v, elapsed: %v", username, err, time.Since(startTime)) 2795 } 2796 providerLog(logger.LevelDebug, "external auth completed for user %#v, elapsed: %v", username, time.Since(startTime)) 2797 if util.IsByteArrayEmpty(out) { 2798 providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %#v id: %v", 2799 username, u.ID) 2800 if u.ID == 0 { 2801 return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) 2802 } 2803 return u, nil 2804 } 2805 err = json.Unmarshal(out, &user) 2806 if err != nil { 2807 return user, fmt.Errorf("invalid external auth response: %v", err) 2808 } 2809 // an empty username means authentication failure 2810 if user.Username == "" { 2811 return user, ErrInvalidCredentials 2812 } 2813 updateUserFromExtAuthResponse(&user, password, pkey) 2814 // some users want to map multiple login usernames with a single SFTPGo account 2815 // for example an SFTP user logins using "user1" or "user2" and the external auth 2816 // returns "user" in both cases, so we use the username returned from 2817 // external auth and not the one used to login 2818 if user.Username != username { 2819 u, err = provider.userExists(user.Username) 2820 } 2821 if u.ID > 0 && err == nil { 2822 user.ID = u.ID 2823 user.UsedQuotaSize = u.UsedQuotaSize 2824 user.UsedQuotaFiles = u.UsedQuotaFiles 2825 user.LastQuotaUpdate = u.LastQuotaUpdate 2826 user.LastLogin = u.LastLogin 2827 user.CreatedAt = u.CreatedAt 2828 user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) 2829 err = provider.updateUser(&user) 2830 if err == nil { 2831 webDAVUsersCache.swap(&user) 2832 cachedPasswords.Add(user.Username, password) 2833 } 2834 return user, err 2835 } 2836 err = provider.addUser(&user) 2837 if err != nil { 2838 return user, err 2839 } 2840 return provider.userExists(user.Username) 2841} 2842 2843func doPluginAuth(username, password string, pubKey []byte, ip, protocol string, 2844 tlsCert *x509.Certificate, authScope int, 2845) (User, error) { 2846 var user User 2847 2848 u, userAsJSON, err := getUserAndJSONForHook(username) 2849 if err != nil { 2850 return user, err 2851 } 2852 2853 if u.Filters.Hooks.ExternalAuthDisabled { 2854 return u, nil 2855 } 2856 2857 pkey, err := util.GetSSHPublicKeyAsString(pubKey) 2858 if err != nil { 2859 return user, err 2860 } 2861 2862 startTime := time.Now() 2863 2864 out, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON) 2865 if err != nil { 2866 return user, fmt.Errorf("plugin auth error for user %#v: %v, elapsed: %v, auth scope: %v", 2867 username, err, time.Since(startTime), authScope) 2868 } 2869 providerLog(logger.LevelDebug, "plugin auth completed for user %#v, elapsed: %v,auth scope: %v", 2870 username, time.Since(startTime), authScope) 2871 if util.IsByteArrayEmpty(out) { 2872 providerLog(logger.LevelDebug, "empty response from plugin auth, no modification requested for user %#v id: %v", 2873 username, u.ID) 2874 if u.ID == 0 { 2875 return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) 2876 } 2877 return u, nil 2878 } 2879 err = json.Unmarshal(out, &user) 2880 if err != nil { 2881 return user, fmt.Errorf("invalid plugin auth response: %v", err) 2882 } 2883 updateUserFromExtAuthResponse(&user, password, pkey) 2884 if u.ID > 0 { 2885 user.ID = u.ID 2886 user.UsedQuotaSize = u.UsedQuotaSize 2887 user.UsedQuotaFiles = u.UsedQuotaFiles 2888 user.LastQuotaUpdate = u.LastQuotaUpdate 2889 user.LastLogin = u.LastLogin 2890 err = provider.updateUser(&user) 2891 if err == nil { 2892 webDAVUsersCache.swap(&user) 2893 cachedPasswords.Add(user.Username, password) 2894 } 2895 return user, err 2896 } 2897 err = provider.addUser(&user) 2898 if err != nil { 2899 return user, err 2900 } 2901 return provider.userExists(user.Username) 2902} 2903 2904func getUserAndJSONForHook(username string) (User, []byte, error) { 2905 var userAsJSON []byte 2906 u, err := provider.userExists(username) 2907 if err != nil { 2908 if _, ok := err.(*util.RecordNotFoundError); !ok { 2909 return u, userAsJSON, err 2910 } 2911 u = User{ 2912 BaseUser: sdk.BaseUser{ 2913 ID: 0, 2914 Username: username, 2915 }, 2916 } 2917 } 2918 userAsJSON, err = json.Marshal(u) 2919 if err != nil { 2920 return u, userAsJSON, err 2921 } 2922 return u, userAsJSON, err 2923} 2924 2925func providerLog(level logger.LogLevel, format string, v ...interface{}) { 2926 logger.Log(level, logSender, "", format, v...) 2927} 2928