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