2024-01-01 10:31:45 +00:00
|
|
|
// Copyright (C) 2019 Nicola Murino
|
2022-07-17 18:16:00 +00:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
|
|
// by the Free Software Foundation, version 3.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
2023-01-03 09:18:30 +00:00
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2022-07-17 18:16:00 +00:00
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
package dataprovider
|
|
|
|
|
|
|
|
import (
|
2021-10-10 11:08:05 +00:00
|
|
|
"encoding/json"
|
2021-01-17 21:29:08 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
2021-07-31 08:39:53 +00:00
|
|
|
"os"
|
2024-07-24 16:27:13 +00:00
|
|
|
"slices"
|
2022-11-01 11:22:54 +00:00
|
|
|
"strconv"
|
2021-01-17 21:29:08 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/alexedwards/argon2id"
|
2022-09-13 16:04:27 +00:00
|
|
|
"github.com/sftpgo/sdk"
|
2021-08-06 16:56:07 +00:00
|
|
|
passwordvalidator "github.com/wagslane/go-password-validator"
|
2021-04-20 11:55:09 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2021-01-17 21:29:08 +00:00
|
|
|
|
2022-07-24 14:18:54 +00:00
|
|
|
"github.com/drakkan/sftpgo/v2/internal/kms"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/mfa"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
2021-01-17 21:29:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Available permissions for SFTPGo admins
|
|
|
|
const (
|
|
|
|
PermAdminAny = "*"
|
|
|
|
PermAdminAddUsers = "add_users"
|
|
|
|
PermAdminChangeUsers = "edit_users"
|
|
|
|
PermAdminDeleteUsers = "del_users"
|
|
|
|
PermAdminViewUsers = "view_users"
|
|
|
|
PermAdminViewConnections = "view_conns"
|
|
|
|
PermAdminCloseConnections = "close_conns"
|
|
|
|
PermAdminViewServerStatus = "view_status"
|
|
|
|
PermAdminManageAdmins = "manage_admins"
|
2022-04-25 13:49:11 +00:00
|
|
|
PermAdminManageGroups = "manage_groups"
|
2023-07-23 16:48:49 +00:00
|
|
|
PermAdminManageFolders = "manage_folders"
|
2021-08-17 16:08:32 +00:00
|
|
|
PermAdminManageAPIKeys = "manage_apikeys"
|
2021-01-17 21:29:08 +00:00
|
|
|
PermAdminQuotaScans = "quota_scans"
|
|
|
|
PermAdminManageSystem = "manage_system"
|
|
|
|
PermAdminManageDefender = "manage_defender"
|
|
|
|
PermAdminViewDefender = "view_defender"
|
2021-09-25 10:20:31 +00:00
|
|
|
PermAdminRetentionChecks = "retention_checks"
|
2021-10-23 13:47:21 +00:00
|
|
|
PermAdminViewEvents = "view_events"
|
2022-07-11 06:17:36 +00:00
|
|
|
PermAdminManageEventRules = "manage_event_rules"
|
2022-11-16 18:04:50 +00:00
|
|
|
PermAdminManageRoles = "manage_roles"
|
2023-02-09 08:33:33 +00:00
|
|
|
PermAdminManageIPLists = "manage_ip_lists"
|
2024-02-23 17:24:07 +00:00
|
|
|
PermAdminDisableMFA = "disable_mfa"
|
2021-01-17 21:29:08 +00:00
|
|
|
)
|
|
|
|
|
2022-09-13 16:04:27 +00:00
|
|
|
const (
|
|
|
|
// GroupAddToUsersAsMembership defines that the admin's group will be added as membership group for new users
|
|
|
|
GroupAddToUsersAsMembership = iota
|
|
|
|
// GroupAddToUsersAsPrimary defines that the admin's group will be added as primary group for new users
|
|
|
|
GroupAddToUsersAsPrimary
|
|
|
|
// GroupAddToUsersAsSecondary defines that the admin's group will be added as secondary group for new users
|
|
|
|
GroupAddToUsersAsSecondary
|
|
|
|
)
|
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
var (
|
|
|
|
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
2023-07-23 16:48:49 +00:00
|
|
|
PermAdminViewUsers, PermAdminManageFolders, PermAdminManageGroups, PermAdminViewConnections,
|
|
|
|
PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles,
|
|
|
|
PermAdminManageEventRules, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
|
|
|
|
PermAdminManageDefender, PermAdminViewDefender, PermAdminManageIPLists, PermAdminRetentionChecks,
|
2024-02-23 17:24:07 +00:00
|
|
|
PermAdminViewEvents, PermAdminDisableMFA}
|
2022-11-16 18:04:50 +00:00
|
|
|
forbiddenPermsForRoleAdmins = []string{PermAdminAny, PermAdminManageAdmins, PermAdminManageSystem,
|
2023-02-09 08:33:33 +00:00
|
|
|
PermAdminManageEventRules, PermAdminManageIPLists, PermAdminManageRoles}
|
2021-01-17 21:29:08 +00:00
|
|
|
)
|
|
|
|
|
2022-01-06 09:11:47 +00:00
|
|
|
// AdminTOTPConfig defines the time-based one time password configuration
|
|
|
|
type AdminTOTPConfig struct {
|
2021-09-04 10:11:04 +00:00
|
|
|
Enabled bool `json:"enabled,omitempty"`
|
|
|
|
ConfigName string `json:"config_name,omitempty"`
|
|
|
|
Secret *kms.Secret `json:"secret,omitempty"`
|
|
|
|
}
|
|
|
|
|
2022-01-06 09:11:47 +00:00
|
|
|
func (c *AdminTOTPConfig) validate(username string) error {
|
2021-09-04 10:11:04 +00:00
|
|
|
if !c.Enabled {
|
|
|
|
c.ConfigName = ""
|
|
|
|
c.Secret = kms.NewEmptySecret()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if c.ConfigName == "" {
|
|
|
|
return util.NewValidationError("totp: config name is mandatory")
|
|
|
|
}
|
2024-07-24 16:27:13 +00:00
|
|
|
if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
|
2023-02-27 18:02:43 +00:00
|
|
|
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
|
2021-09-04 10:11:04 +00:00
|
|
|
}
|
|
|
|
if c.Secret.IsEmpty() {
|
|
|
|
return util.NewValidationError("totp: secret is mandatory")
|
|
|
|
}
|
|
|
|
if c.Secret.IsPlain() {
|
2021-09-05 12:10:12 +00:00
|
|
|
c.Secret.SetAdditionalData(username)
|
2021-09-04 10:11:04 +00:00
|
|
|
if err := c.Secret.Encrypt(); err != nil {
|
|
|
|
return util.NewValidationError(fmt.Sprintf("totp: unable to encrypt secret: %v", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-09-21 17:36:08 +00:00
|
|
|
// AdminPreferences defines the admin preferences
|
|
|
|
type AdminPreferences struct {
|
|
|
|
// Allow to hide some sections from the user page.
|
|
|
|
// These are not security settings and are not enforced server side
|
|
|
|
// in any way. They are only intended to simplify the user page in
|
|
|
|
// the WebAdmin UI.
|
|
|
|
//
|
|
|
|
// 1 means hide groups section
|
|
|
|
// 2 means hide filesystem section, "users_base_dir" must be set in the config file otherwise this setting is ignored
|
|
|
|
// 4 means hide virtual folders section
|
|
|
|
// 8 means hide profile section
|
|
|
|
// 16 means hide ACLs section
|
|
|
|
// 32 means hide disk and bandwidth quota limits section
|
|
|
|
// 64 means hide advanced settings section
|
|
|
|
//
|
|
|
|
// The settings can be combined
|
|
|
|
HideUserPageSections int `json:"hide_user_page_sections,omitempty"`
|
2022-11-05 17:01:24 +00:00
|
|
|
// Defines the default expiration for newly created users as number of days.
|
|
|
|
// 0 means no expiration
|
|
|
|
DefaultUsersExpiration int `json:"default_users_expiration,omitempty"`
|
2022-09-21 17:36:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// HideGroups returns true if the groups section should be hidden
|
|
|
|
func (p *AdminPreferences) HideGroups() bool {
|
|
|
|
return p.HideUserPageSections&1 != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// HideFilesystem returns true if the filesystem section should be hidden
|
|
|
|
func (p *AdminPreferences) HideFilesystem() bool {
|
|
|
|
return config.UsersBaseDir != "" && p.HideUserPageSections&2 != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// HideVirtualFolders returns true if the virtual folder section should be hidden
|
|
|
|
func (p *AdminPreferences) HideVirtualFolders() bool {
|
|
|
|
return p.HideUserPageSections&4 != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// HideProfile returns true if the profile section should be hidden
|
|
|
|
func (p *AdminPreferences) HideProfile() bool {
|
|
|
|
return p.HideUserPageSections&8 != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// HideACLs returns true if the ACLs section should be hidden
|
|
|
|
func (p *AdminPreferences) HideACLs() bool {
|
|
|
|
return p.HideUserPageSections&16 != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// HideDiskQuotaAndBandwidthLimits returns true if the disk quota and bandwidth limits
|
|
|
|
// section should be hidden
|
|
|
|
func (p *AdminPreferences) HideDiskQuotaAndBandwidthLimits() bool {
|
|
|
|
return p.HideUserPageSections&32 != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// HideAdvancedSettings returns true if the advanced settings section should be hidden
|
|
|
|
func (p *AdminPreferences) HideAdvancedSettings() bool {
|
|
|
|
return p.HideUserPageSections&64 != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// VisibleUserPageSections returns the number of visible sections
|
|
|
|
// in the user page
|
|
|
|
func (p *AdminPreferences) VisibleUserPageSections() int {
|
|
|
|
var result int
|
|
|
|
|
|
|
|
if !p.HideProfile() {
|
|
|
|
result++
|
|
|
|
}
|
|
|
|
if !p.HideACLs() {
|
|
|
|
result++
|
|
|
|
}
|
|
|
|
if !p.HideDiskQuotaAndBandwidthLimits() {
|
|
|
|
result++
|
|
|
|
}
|
|
|
|
if !p.HideAdvancedSettings() {
|
|
|
|
result++
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
// AdminFilters defines additional restrictions for SFTPGo admins
|
2021-05-27 07:40:46 +00:00
|
|
|
// TODO: rename to AdminOptions in v3
|
2021-01-17 21:29:08 +00:00
|
|
|
type AdminFilters struct {
|
|
|
|
// only clients connecting from these IP/Mask are allowed.
|
|
|
|
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
|
|
|
|
// for example "192.0.2.0/24" or "2001:db8::/32"
|
|
|
|
AllowList []string `json:"allow_list,omitempty"`
|
2021-08-17 16:08:32 +00:00
|
|
|
// API key auth allows to impersonate this administrator with an API key
|
|
|
|
AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"`
|
2024-02-21 19:45:10 +00:00
|
|
|
// A password change is required at the next login
|
|
|
|
RequirePasswordChange bool `json:"require_password_change,omitempty"`
|
|
|
|
// Require two factor authentication
|
|
|
|
RequireTwoFactor bool `json:"require_two_factor"`
|
2021-09-04 10:11:04 +00:00
|
|
|
// Time-based one time passwords configuration
|
2022-01-06 09:11:47 +00:00
|
|
|
TOTPConfig AdminTOTPConfig `json:"totp_config,omitempty"`
|
2021-09-04 10:11:04 +00:00
|
|
|
// Recovery codes to use if the user loses access to their second factor auth device.
|
|
|
|
// Each code can only be used once, you should use these codes to login and disable or
|
|
|
|
// reset 2FA for your account
|
2022-09-21 17:36:08 +00:00
|
|
|
RecoveryCodes []RecoveryCode `json:"recovery_codes,omitempty"`
|
|
|
|
Preferences AdminPreferences `json:"preferences"`
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
|
2022-09-13 16:04:27 +00:00
|
|
|
// AdminGroupMappingOptions defines the options for admin/group mapping
|
|
|
|
type AdminGroupMappingOptions struct {
|
|
|
|
AddToUsersAs int `json:"add_to_users_as,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *AdminGroupMappingOptions) validate() error {
|
|
|
|
if o.AddToUsersAs < GroupAddToUsersAsMembership || o.AddToUsersAs > GroupAddToUsersAsSecondary {
|
|
|
|
return util.NewValidationError(fmt.Sprintf("Invalid mode to add groups to new users: %d", o.AddToUsersAs))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetUserGroupType returns the type for the matching user group
|
|
|
|
func (o *AdminGroupMappingOptions) GetUserGroupType() int {
|
|
|
|
switch o.AddToUsersAs {
|
|
|
|
case GroupAddToUsersAsPrimary:
|
|
|
|
return sdk.GroupTypePrimary
|
|
|
|
case GroupAddToUsersAsSecondary:
|
|
|
|
return sdk.GroupTypeSecondary
|
|
|
|
default:
|
|
|
|
return sdk.GroupTypeMembership
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AdminGroupMapping defines the mapping between an SFTPGo admin and a group
|
|
|
|
type AdminGroupMapping struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
Options AdminGroupMappingOptions `json:"options"`
|
|
|
|
}
|
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
// Admin defines a SFTPGo admin
|
|
|
|
type Admin struct {
|
|
|
|
// Database unique identifier
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
// 1 enabled, 0 disabled (login is not allowed)
|
|
|
|
Status int `json:"status"`
|
|
|
|
// Username
|
|
|
|
Username string `json:"username"`
|
|
|
|
Password string `json:"password,omitempty"`
|
2021-09-25 17:06:13 +00:00
|
|
|
Email string `json:"email,omitempty"`
|
2021-01-17 21:29:08 +00:00
|
|
|
Permissions []string `json:"permissions"`
|
|
|
|
Filters AdminFilters `json:"filters,omitempty"`
|
2021-02-24 18:40:29 +00:00
|
|
|
Description string `json:"description,omitempty"`
|
2021-01-17 21:29:08 +00:00
|
|
|
AdditionalInfo string `json:"additional_info,omitempty"`
|
2022-09-13 16:04:27 +00:00
|
|
|
// Groups membership
|
|
|
|
Groups []AdminGroupMapping `json:"groups,omitempty"`
|
2021-08-19 13:51:43 +00:00
|
|
|
// Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0
|
|
|
|
CreatedAt int64 `json:"created_at"`
|
|
|
|
// last update time as unix timestamp in milliseconds
|
|
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
|
|
// Last login as unix timestamp in milliseconds
|
|
|
|
LastLogin int64 `json:"last_login"`
|
2022-11-16 18:04:50 +00:00
|
|
|
// Role name. If set the admin can only administer users with the same role.
|
|
|
|
// Role admins cannot have the following permissions:
|
|
|
|
// - manage_admins
|
|
|
|
// - manage_apikeys
|
|
|
|
// - manage_system
|
|
|
|
// - manage_event_rules
|
|
|
|
// - manage_roles
|
|
|
|
Role string `json:"role,omitempty"`
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 10:11:04 +00:00
|
|
|
// CountUnusedRecoveryCodes returns the number of unused recovery codes
|
|
|
|
func (a *Admin) CountUnusedRecoveryCodes() int {
|
|
|
|
unused := 0
|
|
|
|
for _, code := range a.Filters.RecoveryCodes {
|
|
|
|
if !code.Used {
|
|
|
|
unused++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return unused
|
|
|
|
}
|
|
|
|
|
2021-11-06 13:13:20 +00:00
|
|
|
func (a *Admin) hashPassword() error {
|
2021-07-11 13:26:51 +00:00
|
|
|
if a.Password != "" && !util.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) {
|
2021-08-06 16:56:07 +00:00
|
|
|
if config.PasswordValidation.Admins.MinEntropy > 0 {
|
|
|
|
if err := passwordvalidator.Validate(a.Password, config.PasswordValidation.Admins.MinEntropy); err != nil {
|
2024-01-22 19:22:41 +00:00
|
|
|
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
|
2021-08-06 16:56:07 +00:00
|
|
|
}
|
|
|
|
}
|
2021-04-25 07:38:33 +00:00
|
|
|
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
2021-04-20 11:55:09 +00:00
|
|
|
pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-05-08 17:01:58 +00:00
|
|
|
a.Password = util.BytesToString(pwd)
|
2021-04-20 11:55:09 +00:00
|
|
|
} else {
|
|
|
|
pwd, err := argon2id.CreateHash(a.Password, argon2Params)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
a.Password = pwd
|
2021-03-04 08:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-04 10:11:04 +00:00
|
|
|
func (a *Admin) hasRedactedSecret() bool {
|
|
|
|
return a.Filters.TOTPConfig.Secret.IsRedacted()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Admin) validateRecoveryCodes() error {
|
|
|
|
for i := 0; i < len(a.Filters.RecoveryCodes); i++ {
|
|
|
|
code := &a.Filters.RecoveryCodes[i]
|
|
|
|
if code.Secret.IsEmpty() {
|
|
|
|
return util.NewValidationError("mfa: recovery code cannot be empty")
|
|
|
|
}
|
|
|
|
if code.Secret.IsPlain() {
|
2021-09-05 12:10:12 +00:00
|
|
|
code.Secret.SetAdditionalData(a.Username)
|
2021-09-04 10:11:04 +00:00
|
|
|
if err := code.Secret.Encrypt(); err != nil {
|
|
|
|
return util.NewValidationError(fmt.Sprintf("mfa: unable to encrypt recovery code: %v", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Admin) validatePermissions() error {
|
2022-05-31 16:22:18 +00:00
|
|
|
a.Permissions = util.RemoveDuplicates(a.Permissions, false)
|
2021-09-04 10:11:04 +00:00
|
|
|
if len(a.Permissions) == 0 {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(
|
|
|
|
util.NewValidationError("please grant some permissions to this admin"),
|
|
|
|
util.I18nErrorPermissionsRequired,
|
|
|
|
)
|
2021-09-04 10:11:04 +00:00
|
|
|
}
|
2024-07-24 16:27:13 +00:00
|
|
|
if slices.Contains(a.Permissions, PermAdminAny) {
|
2021-09-04 10:11:04 +00:00
|
|
|
a.Permissions = []string{PermAdminAny}
|
|
|
|
}
|
|
|
|
for _, perm := range a.Permissions {
|
2024-07-24 16:27:13 +00:00
|
|
|
if !slices.Contains(validAdminPerms, perm) {
|
2022-11-16 18:04:50 +00:00
|
|
|
return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
|
|
|
|
}
|
|
|
|
if a.Role != "" {
|
2024-07-24 16:27:13 +00:00
|
|
|
if slices.Contains(forbiddenPermsForRoleAdmins, perm) {
|
2024-01-18 18:18:57 +00:00
|
|
|
deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",")
|
|
|
|
return util.NewI18nError(
|
|
|
|
util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)),
|
|
|
|
util.I18nErrorRoleAdminPerms,
|
|
|
|
util.I18nErrorArgs(map[string]any{
|
|
|
|
"val": deniedPerms,
|
|
|
|
}),
|
|
|
|
)
|
2022-11-16 18:04:50 +00:00
|
|
|
}
|
2021-09-04 10:11:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-09-13 16:04:27 +00:00
|
|
|
func (a *Admin) validateGroups() error {
|
|
|
|
hasPrimary := false
|
|
|
|
for _, g := range a.Groups {
|
|
|
|
if g.Name == "" {
|
|
|
|
return util.NewValidationError("group name is mandatory")
|
|
|
|
}
|
|
|
|
if err := g.Options.validate(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if g.Options.AddToUsersAs == GroupAddToUsersAsPrimary {
|
|
|
|
if hasPrimary {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(
|
|
|
|
util.NewValidationError("only one primary group is allowed"),
|
|
|
|
util.I18nErrorPrimaryGroup,
|
|
|
|
)
|
2022-09-13 16:04:27 +00:00
|
|
|
}
|
|
|
|
hasPrimary = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
func (a *Admin) validate() error {
|
2021-09-04 10:11:04 +00:00
|
|
|
a.SetEmptySecretsIfNil()
|
2021-01-17 21:29:08 +00:00
|
|
|
if a.Username == "" {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
2022-09-13 16:04:27 +00:00
|
|
|
if err := checkReservedUsernames(a.Username); err != nil {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(err, util.I18nErrorReservedUsername)
|
2022-09-13 16:04:27 +00:00
|
|
|
}
|
2021-01-17 21:29:08 +00:00
|
|
|
if a.Password == "" {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(util.NewValidationError("please set a password"), util.I18nErrorPasswordRequired)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
2021-09-04 10:11:04 +00:00
|
|
|
if a.hasRedactedSecret() {
|
|
|
|
return util.NewValidationError("cannot save an admin with a redacted secret")
|
|
|
|
}
|
2021-09-05 12:10:12 +00:00
|
|
|
if err := a.Filters.TOTPConfig.validate(a.Username); err != nil {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(err, util.I18nError2FAInvalid)
|
2021-09-04 10:11:04 +00:00
|
|
|
}
|
|
|
|
if err := a.validateRecoveryCodes(); err != nil {
|
2024-01-22 19:22:41 +00:00
|
|
|
return util.NewI18nError(err, util.I18nErrorRecoveryCodesInvalid)
|
2021-09-04 10:11:04 +00:00
|
|
|
}
|
2022-01-31 17:01:37 +00:00
|
|
|
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Username) {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(
|
|
|
|
util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)),
|
|
|
|
util.I18nErrorInvalidUser,
|
|
|
|
)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
2021-11-06 13:13:20 +00:00
|
|
|
if err := a.hashPassword(); err != nil {
|
2021-03-04 08:48:53 +00:00
|
|
|
return err
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
2021-09-04 10:11:04 +00:00
|
|
|
if err := a.validatePermissions(); err != nil {
|
|
|
|
return err
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
2022-05-27 05:39:55 +00:00
|
|
|
if a.Email != "" && !util.IsEmailValid(a.Email) {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(
|
|
|
|
util.NewValidationError(fmt.Sprintf("email %q is not valid", a.Email)),
|
|
|
|
util.I18nErrorInvalidEmail,
|
|
|
|
)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
2022-05-31 16:22:18 +00:00
|
|
|
a.Filters.AllowList = util.RemoveDuplicates(a.Filters.AllowList, false)
|
2021-01-17 21:29:08 +00:00
|
|
|
for _, IPMask := range a.Filters.AllowList {
|
|
|
|
_, _, err := net.ParseCIDR(IPMask)
|
|
|
|
if err != nil {
|
2024-01-18 18:18:57 +00:00
|
|
|
return util.NewI18nError(
|
|
|
|
util.NewValidationError(fmt.Sprintf("could not parse allow list entry %q : %v", IPMask, err)),
|
|
|
|
util.I18nErrorInvalidIPMask,
|
|
|
|
)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-13 16:04:27 +00:00
|
|
|
return a.validateGroups()
|
|
|
|
}
|
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
// CheckPassword verifies the admin password
|
|
|
|
func (a *Admin) CheckPassword(password string) (bool, error) {
|
2023-04-13 16:23:42 +00:00
|
|
|
if config.PasswordCaching {
|
|
|
|
found, match := cachedAdminPasswords.Check(a.Username, password, a.Password)
|
|
|
|
if found {
|
|
|
|
if !match {
|
|
|
|
return false, ErrInvalidCredentials
|
|
|
|
}
|
|
|
|
return match, nil
|
|
|
|
}
|
|
|
|
}
|
2021-04-20 11:55:09 +00:00
|
|
|
if strings.HasPrefix(a.Password, bcryptPwdPrefix) {
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(a.Password), []byte(password)); err != nil {
|
2021-04-26 17:48:21 +00:00
|
|
|
return false, ErrInvalidCredentials
|
2021-04-20 11:55:09 +00:00
|
|
|
}
|
2023-04-13 16:23:42 +00:00
|
|
|
cachedAdminPasswords.Add(a.Username, password, a.Password)
|
2021-04-20 11:55:09 +00:00
|
|
|
return true, nil
|
|
|
|
}
|
2021-11-06 13:13:20 +00:00
|
|
|
match, err := argon2id.ComparePasswordAndHash(password, a.Password)
|
|
|
|
if !match || err != nil {
|
|
|
|
return false, ErrInvalidCredentials
|
|
|
|
}
|
2024-03-02 17:49:07 +00:00
|
|
|
if match {
|
2023-04-13 16:23:42 +00:00
|
|
|
cachedAdminPasswords.Add(a.Username, password, a.Password)
|
|
|
|
}
|
2021-11-06 13:13:20 +00:00
|
|
|
return match, err
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CanLoginFromIP returns true if login from the given IP is allowed
|
|
|
|
func (a *Admin) CanLoginFromIP(ip string) bool {
|
|
|
|
if len(a.Filters.AllowList) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
parsedIP := net.ParseIP(ip)
|
|
|
|
if parsedIP == nil {
|
|
|
|
return len(a.Filters.AllowList) == 0
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ipMask := range a.Filters.AllowList {
|
|
|
|
_, network, err := net.ParseCIDR(ipMask)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if network.Contains(parsedIP) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-08-17 16:08:32 +00:00
|
|
|
// CanLogin returns an error if the login is not allowed
|
|
|
|
func (a *Admin) CanLogin(ip string) error {
|
2021-01-17 21:29:08 +00:00
|
|
|
if a.Status != 1 {
|
2023-02-27 18:02:43 +00:00
|
|
|
return fmt.Errorf("admin %q is disabled", a.Username)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
2021-08-17 16:08:32 +00:00
|
|
|
if !a.CanLoginFromIP(ip) {
|
|
|
|
return fmt.Errorf("login from IP %v not allowed", ip)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Admin) checkUserAndPass(password, ip string) error {
|
|
|
|
if err := a.CanLogin(ip); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-01-17 21:29:08 +00:00
|
|
|
if a.Password == "" || password == "" {
|
|
|
|
return errors.New("credentials cannot be null or empty")
|
|
|
|
}
|
|
|
|
match, err := a.CheckPassword(password)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !match {
|
|
|
|
return ErrInvalidCredentials
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-10-10 11:08:05 +00:00
|
|
|
// RenderAsJSON implements the renderer interface used within plugins
|
|
|
|
func (a *Admin) RenderAsJSON(reload bool) ([]byte, error) {
|
|
|
|
if reload {
|
|
|
|
admin, err := provider.adminExists(a.Username)
|
|
|
|
if err != nil {
|
2021-12-16 18:53:00 +00:00
|
|
|
providerLog(logger.LevelError, "unable to reload admin before rendering as json: %v", err)
|
2021-10-10 11:08:05 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
admin.HideConfidentialData()
|
|
|
|
return json.Marshal(admin)
|
|
|
|
}
|
|
|
|
a.HideConfidentialData()
|
|
|
|
return json.Marshal(a)
|
|
|
|
}
|
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
// HideConfidentialData hides admin confidential data
|
|
|
|
func (a *Admin) HideConfidentialData() {
|
|
|
|
a.Password = ""
|
2021-09-04 10:11:04 +00:00
|
|
|
if a.Filters.TOTPConfig.Secret != nil {
|
|
|
|
a.Filters.TOTPConfig.Secret.Hide()
|
|
|
|
}
|
2021-09-11 12:19:17 +00:00
|
|
|
for _, code := range a.Filters.RecoveryCodes {
|
|
|
|
if code.Secret != nil {
|
|
|
|
code.Secret.Hide()
|
|
|
|
}
|
|
|
|
}
|
2021-09-04 10:11:04 +00:00
|
|
|
a.SetNilSecretsIfEmpty()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetEmptySecretsIfNil sets the secrets to empty if nil
|
|
|
|
func (a *Admin) SetEmptySecretsIfNil() {
|
|
|
|
if a.Filters.TOTPConfig.Secret == nil {
|
|
|
|
a.Filters.TOTPConfig.Secret = kms.NewEmptySecret()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetNilSecretsIfEmpty set the secrets to nil if empty.
|
|
|
|
// This is useful before rendering as JSON so the empty fields
|
|
|
|
// will not be serialized.
|
|
|
|
func (a *Admin) SetNilSecretsIfEmpty() {
|
2021-09-04 11:56:29 +00:00
|
|
|
if a.Filters.TOTPConfig.Secret != nil && a.Filters.TOTPConfig.Secret.IsEmpty() {
|
2021-09-04 10:11:04 +00:00
|
|
|
a.Filters.TOTPConfig.Secret = nil
|
|
|
|
}
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// HasPermission returns true if the admin has the specified permission
|
|
|
|
func (a *Admin) HasPermission(perm string) bool {
|
2024-07-24 16:27:13 +00:00
|
|
|
if slices.Contains(a.Permissions, PermAdminAny) {
|
2021-01-17 21:29:08 +00:00
|
|
|
return true
|
|
|
|
}
|
2024-07-24 16:27:13 +00:00
|
|
|
return slices.Contains(a.Permissions, perm)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetAllowedIPAsString returns the allowed IP as comma separated string
|
|
|
|
func (a *Admin) GetAllowedIPAsString() string {
|
|
|
|
return strings.Join(a.Filters.AllowList, ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetValidPerms returns the allowed admin permissions
|
|
|
|
func (a *Admin) GetValidPerms() []string {
|
|
|
|
return validAdminPerms
|
|
|
|
}
|
|
|
|
|
2021-09-04 10:11:04 +00:00
|
|
|
// CanManageMFA returns true if the admin can add a multi-factor authentication configuration
|
|
|
|
func (a *Admin) CanManageMFA() bool {
|
|
|
|
return len(mfa.GetAvailableTOTPConfigs()) > 0
|
|
|
|
}
|
|
|
|
|
2021-01-17 21:29:08 +00:00
|
|
|
// GetSignature returns a signature for this admin.
|
2022-11-01 11:22:54 +00:00
|
|
|
// It will change after an update
|
2021-01-17 21:29:08 +00:00
|
|
|
func (a *Admin) GetSignature() string {
|
2022-11-01 11:22:54 +00:00
|
|
|
return strconv.FormatInt(a.UpdatedAt, 10)
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Admin) getACopy() Admin {
|
2021-09-04 10:11:04 +00:00
|
|
|
a.SetEmptySecretsIfNil()
|
2021-01-17 21:29:08 +00:00
|
|
|
permissions := make([]string, len(a.Permissions))
|
|
|
|
copy(permissions, a.Permissions)
|
|
|
|
filters := AdminFilters{}
|
|
|
|
filters.AllowList = make([]string, len(a.Filters.AllowList))
|
2021-08-17 16:08:32 +00:00
|
|
|
filters.AllowAPIKeyAuth = a.Filters.AllowAPIKeyAuth
|
2024-02-21 19:45:10 +00:00
|
|
|
filters.RequirePasswordChange = a.Filters.RequirePasswordChange
|
|
|
|
filters.RequireTwoFactor = a.Filters.RequireTwoFactor
|
2021-09-04 10:11:04 +00:00
|
|
|
filters.TOTPConfig.Enabled = a.Filters.TOTPConfig.Enabled
|
|
|
|
filters.TOTPConfig.ConfigName = a.Filters.TOTPConfig.ConfigName
|
|
|
|
filters.TOTPConfig.Secret = a.Filters.TOTPConfig.Secret.Clone()
|
2021-01-17 21:29:08 +00:00
|
|
|
copy(filters.AllowList, a.Filters.AllowList)
|
2022-01-06 09:11:47 +00:00
|
|
|
filters.RecoveryCodes = make([]RecoveryCode, 0)
|
2021-09-04 10:11:04 +00:00
|
|
|
for _, code := range a.Filters.RecoveryCodes {
|
|
|
|
if code.Secret == nil {
|
|
|
|
code.Secret = kms.NewEmptySecret()
|
|
|
|
}
|
2022-01-06 09:11:47 +00:00
|
|
|
filters.RecoveryCodes = append(filters.RecoveryCodes, RecoveryCode{
|
2021-09-04 10:11:04 +00:00
|
|
|
Secret: code.Secret.Clone(),
|
|
|
|
Used: code.Used,
|
|
|
|
})
|
|
|
|
}
|
2022-09-21 17:36:08 +00:00
|
|
|
filters.Preferences = AdminPreferences{
|
2022-11-05 17:01:24 +00:00
|
|
|
HideUserPageSections: a.Filters.Preferences.HideUserPageSections,
|
|
|
|
DefaultUsersExpiration: a.Filters.Preferences.DefaultUsersExpiration,
|
2022-09-21 17:36:08 +00:00
|
|
|
}
|
2022-09-13 16:04:27 +00:00
|
|
|
groups := make([]AdminGroupMapping, 0, len(a.Groups))
|
|
|
|
for _, g := range a.Groups {
|
|
|
|
groups = append(groups, AdminGroupMapping{
|
|
|
|
Name: g.Name,
|
|
|
|
Options: AdminGroupMappingOptions{
|
|
|
|
AddToUsersAs: g.Options.AddToUsersAs,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2021-01-17 21:29:08 +00:00
|
|
|
|
|
|
|
return Admin{
|
|
|
|
ID: a.ID,
|
|
|
|
Status: a.Status,
|
|
|
|
Username: a.Username,
|
|
|
|
Password: a.Password,
|
|
|
|
Email: a.Email,
|
|
|
|
Permissions: permissions,
|
2022-09-13 16:04:27 +00:00
|
|
|
Groups: groups,
|
2021-01-17 21:29:08 +00:00
|
|
|
Filters: filters,
|
|
|
|
AdditionalInfo: a.AdditionalInfo,
|
2021-02-24 18:40:29 +00:00
|
|
|
Description: a.Description,
|
2021-08-19 13:51:43 +00:00
|
|
|
LastLogin: a.LastLogin,
|
|
|
|
CreatedAt: a.CreatedAt,
|
|
|
|
UpdatedAt: a.UpdatedAt,
|
2022-11-16 18:04:50 +00:00
|
|
|
Role: a.Role,
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-20 08:37:51 +00:00
|
|
|
func (a *Admin) setFromEnv() error {
|
|
|
|
envUsername := strings.TrimSpace(os.Getenv("SFTPGO_DEFAULT_ADMIN_USERNAME"))
|
|
|
|
envPassword := strings.TrimSpace(os.Getenv("SFTPGO_DEFAULT_ADMIN_PASSWORD"))
|
|
|
|
if envUsername == "" || envPassword == "" {
|
|
|
|
return errors.New(`to create the default admin you need to set the env vars "SFTPGO_DEFAULT_ADMIN_USERNAME" and "SFTPGO_DEFAULT_ADMIN_PASSWORD"`)
|
2021-07-31 08:39:53 +00:00
|
|
|
}
|
2021-08-20 08:37:51 +00:00
|
|
|
a.Username = envUsername
|
|
|
|
a.Password = envPassword
|
2021-01-17 21:29:08 +00:00
|
|
|
a.Status = 1
|
|
|
|
a.Permissions = []string{PermAdminAny}
|
2021-08-20 08:37:51 +00:00
|
|
|
return nil
|
2021-01-17 21:29:08 +00:00
|
|
|
}
|