1package dataprovider 2 3import ( 4 "crypto/sha256" 5 "encoding/base64" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net" 10 "os" 11 "regexp" 12 "strings" 13 14 "github.com/alexedwards/argon2id" 15 passwordvalidator "github.com/wagslane/go-password-validator" 16 "golang.org/x/crypto/bcrypt" 17 18 "github.com/drakkan/sftpgo/v2/kms" 19 "github.com/drakkan/sftpgo/v2/logger" 20 "github.com/drakkan/sftpgo/v2/mfa" 21 "github.com/drakkan/sftpgo/v2/sdk" 22 "github.com/drakkan/sftpgo/v2/util" 23) 24 25// Available permissions for SFTPGo admins 26const ( 27 PermAdminAny = "*" 28 PermAdminAddUsers = "add_users" 29 PermAdminChangeUsers = "edit_users" 30 PermAdminDeleteUsers = "del_users" 31 PermAdminViewUsers = "view_users" 32 PermAdminViewConnections = "view_conns" 33 PermAdminCloseConnections = "close_conns" 34 PermAdminViewServerStatus = "view_status" 35 PermAdminManageAdmins = "manage_admins" 36 PermAdminManageAPIKeys = "manage_apikeys" 37 PermAdminQuotaScans = "quota_scans" 38 PermAdminManageSystem = "manage_system" 39 PermAdminManageDefender = "manage_defender" 40 PermAdminViewDefender = "view_defender" 41 PermAdminRetentionChecks = "retention_checks" 42 PermAdminViewEvents = "view_events" 43) 44 45var ( 46 emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$") 47 validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers, 48 PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus, 49 PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem, 50 PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks, PermAdminViewEvents} 51) 52 53// TOTPConfig defines the time-based one time password configuration 54type TOTPConfig struct { 55 Enabled bool `json:"enabled,omitempty"` 56 ConfigName string `json:"config_name,omitempty"` 57 Secret *kms.Secret `json:"secret,omitempty"` 58} 59 60func (c *TOTPConfig) validate(username string) error { 61 if !c.Enabled { 62 c.ConfigName = "" 63 c.Secret = kms.NewEmptySecret() 64 return nil 65 } 66 if c.ConfigName == "" { 67 return util.NewValidationError("totp: config name is mandatory") 68 } 69 if !util.IsStringInSlice(c.ConfigName, mfa.GetAvailableTOTPConfigNames()) { 70 return util.NewValidationError(fmt.Sprintf("totp: config name %#v not found", c.ConfigName)) 71 } 72 if c.Secret.IsEmpty() { 73 return util.NewValidationError("totp: secret is mandatory") 74 } 75 if c.Secret.IsPlain() { 76 c.Secret.SetAdditionalData(username) 77 if err := c.Secret.Encrypt(); err != nil { 78 return util.NewValidationError(fmt.Sprintf("totp: unable to encrypt secret: %v", err)) 79 } 80 } 81 return nil 82} 83 84// AdminFilters defines additional restrictions for SFTPGo admins 85// TODO: rename to AdminOptions in v3 86type AdminFilters struct { 87 // only clients connecting from these IP/Mask are allowed. 88 // IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291 89 // for example "192.0.2.0/24" or "2001:db8::/32" 90 AllowList []string `json:"allow_list,omitempty"` 91 // API key auth allows to impersonate this administrator with an API key 92 AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"` 93 // Time-based one time passwords configuration 94 TOTPConfig TOTPConfig `json:"totp_config,omitempty"` 95 // Recovery codes to use if the user loses access to their second factor auth device. 96 // Each code can only be used once, you should use these codes to login and disable or 97 // reset 2FA for your account 98 RecoveryCodes []sdk.RecoveryCode `json:"recovery_codes,omitempty"` 99} 100 101// Admin defines a SFTPGo admin 102type Admin struct { 103 // Database unique identifier 104 ID int64 `json:"id"` 105 // 1 enabled, 0 disabled (login is not allowed) 106 Status int `json:"status"` 107 // Username 108 Username string `json:"username"` 109 Password string `json:"password,omitempty"` 110 Email string `json:"email,omitempty"` 111 Permissions []string `json:"permissions"` 112 Filters AdminFilters `json:"filters,omitempty"` 113 Description string `json:"description,omitempty"` 114 AdditionalInfo string `json:"additional_info,omitempty"` 115 // Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0 116 CreatedAt int64 `json:"created_at"` 117 // last update time as unix timestamp in milliseconds 118 UpdatedAt int64 `json:"updated_at"` 119 // Last login as unix timestamp in milliseconds 120 LastLogin int64 `json:"last_login"` 121} 122 123// CountUnusedRecoveryCodes returns the number of unused recovery codes 124func (a *Admin) CountUnusedRecoveryCodes() int { 125 unused := 0 126 for _, code := range a.Filters.RecoveryCodes { 127 if !code.Used { 128 unused++ 129 } 130 } 131 return unused 132} 133 134func (a *Admin) hashPassword() error { 135 if a.Password != "" && !util.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) { 136 if config.PasswordValidation.Admins.MinEntropy > 0 { 137 if err := passwordvalidator.Validate(a.Password, config.PasswordValidation.Admins.MinEntropy); err != nil { 138 return util.NewValidationError(err.Error()) 139 } 140 } 141 if config.PasswordHashing.Algo == HashingAlgoBcrypt { 142 pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost) 143 if err != nil { 144 return err 145 } 146 a.Password = string(pwd) 147 } else { 148 pwd, err := argon2id.CreateHash(a.Password, argon2Params) 149 if err != nil { 150 return err 151 } 152 a.Password = pwd 153 } 154 } 155 return nil 156} 157 158func (a *Admin) hasRedactedSecret() bool { 159 return a.Filters.TOTPConfig.Secret.IsRedacted() 160} 161 162func (a *Admin) validateRecoveryCodes() error { 163 for i := 0; i < len(a.Filters.RecoveryCodes); i++ { 164 code := &a.Filters.RecoveryCodes[i] 165 if code.Secret.IsEmpty() { 166 return util.NewValidationError("mfa: recovery code cannot be empty") 167 } 168 if code.Secret.IsPlain() { 169 code.Secret.SetAdditionalData(a.Username) 170 if err := code.Secret.Encrypt(); err != nil { 171 return util.NewValidationError(fmt.Sprintf("mfa: unable to encrypt recovery code: %v", err)) 172 } 173 } 174 } 175 return nil 176} 177 178func (a *Admin) validatePermissions() error { 179 a.Permissions = util.RemoveDuplicates(a.Permissions) 180 if len(a.Permissions) == 0 { 181 return util.NewValidationError("please grant some permissions to this admin") 182 } 183 if util.IsStringInSlice(PermAdminAny, a.Permissions) { 184 a.Permissions = []string{PermAdminAny} 185 } 186 for _, perm := range a.Permissions { 187 if !util.IsStringInSlice(perm, validAdminPerms) { 188 return util.NewValidationError(fmt.Sprintf("invalid permission: %#v", perm)) 189 } 190 } 191 return nil 192} 193 194func (a *Admin) validate() error { 195 a.SetEmptySecretsIfNil() 196 if a.Username == "" { 197 return util.NewValidationError("username is mandatory") 198 } 199 if a.Password == "" { 200 return util.NewValidationError("please set a password") 201 } 202 if a.hasRedactedSecret() { 203 return util.NewValidationError("cannot save an admin with a redacted secret") 204 } 205 if err := a.Filters.TOTPConfig.validate(a.Username); err != nil { 206 return err 207 } 208 if err := a.validateRecoveryCodes(); err != nil { 209 return err 210 } 211 if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(a.Username) { 212 return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)) 213 } 214 if err := a.hashPassword(); err != nil { 215 return err 216 } 217 if err := a.validatePermissions(); err != nil { 218 return err 219 } 220 if a.Email != "" && !emailRegex.MatchString(a.Email) { 221 return util.NewValidationError(fmt.Sprintf("email %#v is not valid", a.Email)) 222 } 223 a.Filters.AllowList = util.RemoveDuplicates(a.Filters.AllowList) 224 for _, IPMask := range a.Filters.AllowList { 225 _, _, err := net.ParseCIDR(IPMask) 226 if err != nil { 227 return util.NewValidationError(fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)) 228 } 229 } 230 231 return nil 232} 233 234// CheckPassword verifies the admin password 235func (a *Admin) CheckPassword(password string) (bool, error) { 236 if strings.HasPrefix(a.Password, bcryptPwdPrefix) { 237 if err := bcrypt.CompareHashAndPassword([]byte(a.Password), []byte(password)); err != nil { 238 return false, ErrInvalidCredentials 239 } 240 return true, nil 241 } 242 match, err := argon2id.ComparePasswordAndHash(password, a.Password) 243 if !match || err != nil { 244 return false, ErrInvalidCredentials 245 } 246 return match, err 247} 248 249// CanLoginFromIP returns true if login from the given IP is allowed 250func (a *Admin) CanLoginFromIP(ip string) bool { 251 if len(a.Filters.AllowList) == 0 { 252 return true 253 } 254 parsedIP := net.ParseIP(ip) 255 if parsedIP == nil { 256 return len(a.Filters.AllowList) == 0 257 } 258 259 for _, ipMask := range a.Filters.AllowList { 260 _, network, err := net.ParseCIDR(ipMask) 261 if err != nil { 262 continue 263 } 264 if network.Contains(parsedIP) { 265 return true 266 } 267 } 268 return false 269} 270 271// CanLogin returns an error if the login is not allowed 272func (a *Admin) CanLogin(ip string) error { 273 if a.Status != 1 { 274 return fmt.Errorf("admin %#v is disabled", a.Username) 275 } 276 if !a.CanLoginFromIP(ip) { 277 return fmt.Errorf("login from IP %v not allowed", ip) 278 } 279 return nil 280} 281 282func (a *Admin) checkUserAndPass(password, ip string) error { 283 if err := a.CanLogin(ip); err != nil { 284 return err 285 } 286 if a.Password == "" || password == "" { 287 return errors.New("credentials cannot be null or empty") 288 } 289 match, err := a.CheckPassword(password) 290 if err != nil { 291 return err 292 } 293 if !match { 294 return ErrInvalidCredentials 295 } 296 return nil 297} 298 299// RenderAsJSON implements the renderer interface used within plugins 300func (a *Admin) RenderAsJSON(reload bool) ([]byte, error) { 301 if reload { 302 admin, err := provider.adminExists(a.Username) 303 if err != nil { 304 providerLog(logger.LevelWarn, "unable to reload admin before rendering as json: %v", err) 305 return nil, err 306 } 307 admin.HideConfidentialData() 308 return json.Marshal(admin) 309 } 310 a.HideConfidentialData() 311 return json.Marshal(a) 312} 313 314// HideConfidentialData hides admin confidential data 315func (a *Admin) HideConfidentialData() { 316 a.Password = "" 317 if a.Filters.TOTPConfig.Secret != nil { 318 a.Filters.TOTPConfig.Secret.Hide() 319 } 320 for _, code := range a.Filters.RecoveryCodes { 321 if code.Secret != nil { 322 code.Secret.Hide() 323 } 324 } 325 a.SetNilSecretsIfEmpty() 326} 327 328// SetEmptySecretsIfNil sets the secrets to empty if nil 329func (a *Admin) SetEmptySecretsIfNil() { 330 if a.Filters.TOTPConfig.Secret == nil { 331 a.Filters.TOTPConfig.Secret = kms.NewEmptySecret() 332 } 333} 334 335// SetNilSecretsIfEmpty set the secrets to nil if empty. 336// This is useful before rendering as JSON so the empty fields 337// will not be serialized. 338func (a *Admin) SetNilSecretsIfEmpty() { 339 if a.Filters.TOTPConfig.Secret != nil && a.Filters.TOTPConfig.Secret.IsEmpty() { 340 a.Filters.TOTPConfig.Secret = nil 341 } 342} 343 344// HasPermission returns true if the admin has the specified permission 345func (a *Admin) HasPermission(perm string) bool { 346 if util.IsStringInSlice(PermAdminAny, a.Permissions) { 347 return true 348 } 349 return util.IsStringInSlice(perm, a.Permissions) 350} 351 352// GetPermissionsAsString returns permission as string 353func (a *Admin) GetPermissionsAsString() string { 354 return strings.Join(a.Permissions, ", ") 355} 356 357// GetAllowedIPAsString returns the allowed IP as comma separated string 358func (a *Admin) GetAllowedIPAsString() string { 359 return strings.Join(a.Filters.AllowList, ",") 360} 361 362// GetValidPerms returns the allowed admin permissions 363func (a *Admin) GetValidPerms() []string { 364 return validAdminPerms 365} 366 367// GetInfoString returns admin's info as string. 368func (a *Admin) GetInfoString() string { 369 var result strings.Builder 370 if a.Email != "" { 371 result.WriteString(fmt.Sprintf("Email: %v. ", a.Email)) 372 } 373 if len(a.Filters.AllowList) > 0 { 374 result.WriteString(fmt.Sprintf("Allowed IP/Mask: %v. ", len(a.Filters.AllowList))) 375 } 376 return result.String() 377} 378 379// CanManageMFA returns true if the admin can add a multi-factor authentication configuration 380func (a *Admin) CanManageMFA() bool { 381 return len(mfa.GetAvailableTOTPConfigs()) > 0 382} 383 384// GetSignature returns a signature for this admin. 385// It could change after an update 386func (a *Admin) GetSignature() string { 387 data := []byte(a.Username) 388 data = append(data, []byte(a.Password)...) 389 signature := sha256.Sum256(data) 390 return base64.StdEncoding.EncodeToString(signature[:]) 391} 392 393func (a *Admin) getACopy() Admin { 394 a.SetEmptySecretsIfNil() 395 permissions := make([]string, len(a.Permissions)) 396 copy(permissions, a.Permissions) 397 filters := AdminFilters{} 398 filters.AllowList = make([]string, len(a.Filters.AllowList)) 399 filters.AllowAPIKeyAuth = a.Filters.AllowAPIKeyAuth 400 filters.TOTPConfig.Enabled = a.Filters.TOTPConfig.Enabled 401 filters.TOTPConfig.ConfigName = a.Filters.TOTPConfig.ConfigName 402 filters.TOTPConfig.Secret = a.Filters.TOTPConfig.Secret.Clone() 403 copy(filters.AllowList, a.Filters.AllowList) 404 filters.RecoveryCodes = make([]sdk.RecoveryCode, 0) 405 for _, code := range a.Filters.RecoveryCodes { 406 if code.Secret == nil { 407 code.Secret = kms.NewEmptySecret() 408 } 409 filters.RecoveryCodes = append(filters.RecoveryCodes, sdk.RecoveryCode{ 410 Secret: code.Secret.Clone(), 411 Used: code.Used, 412 }) 413 } 414 415 return Admin{ 416 ID: a.ID, 417 Status: a.Status, 418 Username: a.Username, 419 Password: a.Password, 420 Email: a.Email, 421 Permissions: permissions, 422 Filters: filters, 423 AdditionalInfo: a.AdditionalInfo, 424 Description: a.Description, 425 LastLogin: a.LastLogin, 426 CreatedAt: a.CreatedAt, 427 UpdatedAt: a.UpdatedAt, 428 } 429} 430 431func (a *Admin) setFromEnv() error { 432 envUsername := strings.TrimSpace(os.Getenv("SFTPGO_DEFAULT_ADMIN_USERNAME")) 433 envPassword := strings.TrimSpace(os.Getenv("SFTPGO_DEFAULT_ADMIN_PASSWORD")) 434 if envUsername == "" || envPassword == "" { 435 return errors.New(`to create the default admin you need to set the env vars "SFTPGO_DEFAULT_ADMIN_USERNAME" and "SFTPGO_DEFAULT_ADMIN_PASSWORD"`) 436 } 437 a.Username = envUsername 438 a.Password = envPassword 439 a.Status = 1 440 a.Permissions = []string{PermAdminAny} 441 return nil 442} 443