mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
46998252e5
argon2id has a high memory cost and, if not properly tuned, it can lead to resource starvation. Advanced users can still configure and use argon2id. Passwords stored as argon2id will continue to work
252 lines
8.4 KiB
Go
252 lines
8.4 KiB
Go
package dataprovider
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/alexedwards/argon2id"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/drakkan/sftpgo/utils"
|
|
)
|
|
|
|
// 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"
|
|
PermAdminQuotaScans = "quota_scans"
|
|
PermAdminManageSystem = "manage_system"
|
|
PermAdminManageDefender = "manage_defender"
|
|
PermAdminViewDefender = "view_defender"
|
|
)
|
|
|
|
var (
|
|
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}])))\\.?$")
|
|
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
|
PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
|
|
PermAdminManageAdmins, PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender,
|
|
PermAdminViewDefender}
|
|
)
|
|
|
|
// AdminFilters defines additional restrictions for SFTPGo admins
|
|
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"`
|
|
}
|
|
|
|
// 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"`
|
|
Email string `json:"email"`
|
|
Permissions []string `json:"permissions"`
|
|
Filters AdminFilters `json:"filters,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
AdditionalInfo string `json:"additional_info,omitempty"`
|
|
}
|
|
|
|
func (a *Admin) checkPassword() error {
|
|
if a.Password != "" && !strings.HasPrefix(a.Password, argonPwdPrefix) {
|
|
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
|
pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.Password = string(pwd)
|
|
} else {
|
|
pwd, err := argon2id.CreateHash(a.Password, argon2Params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.Password = pwd
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Admin) validate() error {
|
|
if a.Username == "" {
|
|
return &ValidationError{err: "username is mandatory"}
|
|
}
|
|
if a.Password == "" {
|
|
return &ValidationError{err: "please set a password"}
|
|
}
|
|
if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(a.Username) {
|
|
return &ValidationError{err: fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)}
|
|
}
|
|
if err := a.checkPassword(); err != nil {
|
|
return err
|
|
}
|
|
a.Permissions = utils.RemoveDuplicates(a.Permissions)
|
|
if len(a.Permissions) == 0 {
|
|
return &ValidationError{err: "please grant some permissions to this admin"}
|
|
}
|
|
if utils.IsStringInSlice(PermAdminAny, a.Permissions) {
|
|
a.Permissions = []string{PermAdminAny}
|
|
}
|
|
for _, perm := range a.Permissions {
|
|
if !utils.IsStringInSlice(perm, validAdminPerms) {
|
|
return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", perm)}
|
|
}
|
|
}
|
|
if a.Email != "" && !emailRegex.MatchString(a.Email) {
|
|
return &ValidationError{err: fmt.Sprintf("email %#v is not valid", a.Email)}
|
|
}
|
|
for _, IPMask := range a.Filters.AllowList {
|
|
_, _, err := net.ParseCIDR(IPMask)
|
|
if err != nil {
|
|
return &ValidationError{err: fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckPassword verifies the admin password
|
|
func (a *Admin) CheckPassword(password string) (bool, error) {
|
|
if strings.HasPrefix(a.Password, bcryptPwdPrefix) {
|
|
if err := bcrypt.CompareHashAndPassword([]byte(a.Password), []byte(password)); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
return argon2id.ComparePasswordAndHash(password, a.Password)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (a *Admin) checkUserAndPass(password, ip string) error {
|
|
if a.Status != 1 {
|
|
return fmt.Errorf("admin %#v is disabled", a.Username)
|
|
}
|
|
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
|
|
}
|
|
if !a.CanLoginFromIP(ip) {
|
|
return fmt.Errorf("login from IP %v not allowed", ip)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HideConfidentialData hides admin confidential data
|
|
func (a *Admin) HideConfidentialData() {
|
|
a.Password = ""
|
|
}
|
|
|
|
// HasPermission returns true if the admin has the specified permission
|
|
func (a *Admin) HasPermission(perm string) bool {
|
|
if utils.IsStringInSlice(PermAdminAny, a.Permissions) {
|
|
return true
|
|
}
|
|
return utils.IsStringInSlice(perm, a.Permissions)
|
|
}
|
|
|
|
// GetPermissionsAsString returns permission as string
|
|
func (a *Admin) GetPermissionsAsString() string {
|
|
return strings.Join(a.Permissions, ", ")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// GetInfoString returns admin's info as string.
|
|
func (a *Admin) GetInfoString() string {
|
|
var result string
|
|
if a.Email != "" {
|
|
result = fmt.Sprintf("Email: %v. ", a.Email)
|
|
}
|
|
if len(a.Filters.AllowList) > 0 {
|
|
result += fmt.Sprintf("Allowed IP/Mask: %v. ", len(a.Filters.AllowList))
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetSignature returns a signature for this admin.
|
|
// It could change after an update
|
|
func (a *Admin) GetSignature() string {
|
|
data := []byte(a.Username)
|
|
data = append(data, []byte(a.Password)...)
|
|
signature := sha256.Sum256(data)
|
|
return base64.StdEncoding.EncodeToString(signature[:])
|
|
}
|
|
|
|
func (a *Admin) getACopy() Admin {
|
|
permissions := make([]string, len(a.Permissions))
|
|
copy(permissions, a.Permissions)
|
|
filters := AdminFilters{}
|
|
filters.AllowList = make([]string, len(a.Filters.AllowList))
|
|
copy(filters.AllowList, a.Filters.AllowList)
|
|
|
|
return Admin{
|
|
ID: a.ID,
|
|
Status: a.Status,
|
|
Username: a.Username,
|
|
Password: a.Password,
|
|
Email: a.Email,
|
|
Permissions: permissions,
|
|
Filters: filters,
|
|
AdditionalInfo: a.AdditionalInfo,
|
|
Description: a.Description,
|
|
}
|
|
}
|
|
|
|
// setDefaults sets the appropriate value for the default admin
|
|
func (a *Admin) setDefaults() {
|
|
a.Username = "admin"
|
|
a.Password = "password"
|
|
a.Status = 1
|
|
a.Permissions = []string{PermAdminAny}
|
|
}
|