sftpgo-mirror/internal/dataprovider/user.go
Nicola Murino 184b99d500
user: add a field to indicate whether the password is set
A structure similar to the one used for secrets would be better,
but we don't want to break backwards compatibility.

Also document that omitting the password field in the request body
will preserve the current password when updating a user using the
REST API. Added a test case for this.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-04-06 18:22:09 +02:00

1976 lines
60 KiB
Go

// Copyright (C) 2019-2023 Nicola Murino
//
// 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
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package dataprovider
import (
"encoding/json"
"errors"
"fmt"
"math"
"net"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"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/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/vfs"
)
// Available permissions for SFTPGo users
const (
// All permissions are granted
PermAny = "*"
// List items such as files and directories is allowed
PermListItems = "list"
// download files is allowed
PermDownload = "download"
// upload files is allowed
PermUpload = "upload"
// overwrite an existing file, while uploading, is allowed
// upload permission is required to allow file overwrite
PermOverwrite = "overwrite"
// delete files or directories is allowed
PermDelete = "delete"
// delete files is allowed
PermDeleteFiles = "delete_files"
// delete directories is allowed
PermDeleteDirs = "delete_dirs"
// rename files or directories is allowed
PermRename = "rename"
// rename files is allowed
PermRenameFiles = "rename_files"
// rename directories is allowed
PermRenameDirs = "rename_dirs"
// create directories is allowed
PermCreateDirs = "create_dirs"
// create symbolic links is allowed
PermCreateSymlinks = "create_symlinks"
// changing file or directory permissions is allowed
PermChmod = "chmod"
// changing file or directory owner and group is allowed
PermChown = "chown"
// changing file or directory access and modification time is allowed
PermChtimes = "chtimes"
)
// Available login methods
const (
LoginMethodNoAuthTryed = "no_auth_tryed"
LoginMethodPassword = "password"
SSHLoginMethodPassword = "password-over-SSH"
SSHLoginMethodPublicKey = "publickey"
SSHLoginMethodKeyboardInteractive = "keyboard-interactive"
SSHLoginMethodKeyAndPassword = "publickey+password"
SSHLoginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive"
LoginMethodTLSCertificate = "TLSCertificate"
LoginMethodTLSCertificateAndPwd = "TLSCertificate+password"
LoginMethodIDP = "IDP"
)
var (
errNoMatchingVirtualFolder = errors.New("no matching virtual folder found")
permsRenameAny = []string{PermRename, PermRenameDirs, PermRenameFiles}
permsDeleteAny = []string{PermDelete, PermDeleteDirs, PermDeleteFiles}
)
// RecoveryCode defines a 2FA recovery code
type RecoveryCode struct {
Secret *kms.Secret `json:"secret"`
Used bool `json:"used,omitempty"`
}
// UserTOTPConfig defines the time-based one time password configuration
type UserTOTPConfig struct {
Enabled bool `json:"enabled,omitempty"`
ConfigName string `json:"config_name,omitempty"`
Secret *kms.Secret `json:"secret,omitempty"`
// TOTP will be required for the specified protocols.
// SSH protocol (SFTP/SCP/SSH commands) will ask for the TOTP passcode if the client uses keyboard interactive
// authentication.
// FTP have no standard way to support two factor authentication, if you
// enable the support for this protocol you have to add the TOTP passcode after the password.
// For example if your password is "password" and your one time passcode is
// "123456" you have to use "password123456" as password.
Protocols []string `json:"protocols,omitempty"`
}
// UserFilters defines additional restrictions for a user
// TODO: rename to UserOptions in v3
type UserFilters struct {
sdk.BaseUserFilters
// User must change password from WebClient/REST API at next login.
RequirePasswordChange bool `json:"require_password_change,omitempty"`
// Time-based one time passwords configuration
TOTPConfig UserTOTPConfig `json:"totp_config,omitempty"`
// 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
RecoveryCodes []RecoveryCode `json:"recovery_codes,omitempty"`
}
// User defines a SFTPGo user
type User struct {
sdk.BaseUser
// Additional restrictions
Filters UserFilters `json:"filters"`
// Mapping between virtual paths and virtual folders
VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"`
// Filesystem configuration details
FsConfig vfs.Filesystem `json:"filesystem"`
// groups associated with this user
Groups []sdk.GroupMapping `json:"groups,omitempty"`
// we store the filesystem here using the base path as key.
fsCache map[string]vfs.Fs `json:"-"`
// true if group settings are already applied for this user
groupSettingsApplied bool `json:"-"`
// in multi node setups we mark the user as deleted to be able to update the webdav cache
DeletedAt int64 `json:"-"`
}
// GetFilesystem returns the base filesystem for this user
func (u *User) GetFilesystem(connectionID string) (fs vfs.Fs, err error) {
return u.GetFilesystemForPath("/", connectionID)
}
func (u *User) getRootFs(connectionID string) (fs vfs.Fs, err error) {
switch u.FsConfig.Provider {
case sdk.S3FilesystemProvider:
return vfs.NewS3Fs(connectionID, u.GetHomeDir(), "", u.FsConfig.S3Config)
case sdk.GCSFilesystemProvider:
return vfs.NewGCSFs(connectionID, u.GetHomeDir(), "", u.FsConfig.GCSConfig)
case sdk.AzureBlobFilesystemProvider:
return vfs.NewAzBlobFs(connectionID, u.GetHomeDir(), "", u.FsConfig.AzBlobConfig)
case sdk.CryptedFilesystemProvider:
return vfs.NewCryptFs(connectionID, u.GetHomeDir(), "", u.FsConfig.CryptConfig)
case sdk.SFTPFilesystemProvider:
forbiddenSelfUsers, err := u.getForbiddenSFTPSelfUsers(u.FsConfig.SFTPConfig.Username)
if err != nil {
return nil, err
}
forbiddenSelfUsers = append(forbiddenSelfUsers, u.Username)
return vfs.NewSFTPFs(connectionID, "", u.GetHomeDir(), forbiddenSelfUsers, u.FsConfig.SFTPConfig)
case sdk.HTTPFilesystemProvider:
return vfs.NewHTTPFs(connectionID, u.GetHomeDir(), "", u.FsConfig.HTTPConfig)
default:
return vfs.NewOsFs(connectionID, u.GetHomeDir(), ""), nil
}
}
func (u *User) checkDirWithParents(virtualDirPath, connectionID string) error {
dirs := util.GetDirsForVirtualPath(virtualDirPath)
for idx := len(dirs) - 1; idx >= 0; idx-- {
vPath := dirs[idx]
if vPath == "/" {
continue
}
fs, err := u.GetFilesystemForPath(vPath, connectionID)
if err != nil {
return fmt.Errorf("unable to get fs for path %q: %w", vPath, err)
}
if fs.HasVirtualFolders() {
continue
}
fsPath, err := fs.ResolvePath(vPath)
if err != nil {
return fmt.Errorf("unable to resolve path %q: %w", vPath, err)
}
_, err = fs.Stat(fsPath)
if err == nil {
continue
}
if fs.IsNotExist(err) {
err = fs.Mkdir(fsPath)
if err != nil {
return err
}
vfs.SetPathPermissions(fs, fsPath, u.GetUID(), u.GetGID())
} else {
return fmt.Errorf("unable to stat path %q: %w", vPath, err)
}
}
return nil
}
func (u *User) checkLocalHomeDir(connectionID string) {
switch u.FsConfig.Provider {
case sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider:
return
default:
osFs := vfs.NewOsFs(connectionID, u.GetHomeDir(), "")
osFs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
}
}
func (u *User) checkRootPath(connectionID string) error {
fs, err := u.GetFilesystemForPath("/", connectionID)
if err != nil {
logger.Warn(logSender, connectionID, "could not create main filesystem for user %q err: %v", u.Username, err)
return fmt.Errorf("could not create root filesystem: %w", err)
}
fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
return nil
}
// CheckFsRoot check the root directory for the main fs and the virtual folders.
// It returns an error if the main filesystem cannot be created
func (u *User) CheckFsRoot(connectionID string) error {
if u.Filters.DisableFsChecks {
return nil
}
delay := lastLoginMinDelay
if u.Filters.ExternalAuthCacheTime > 0 {
cacheTime := time.Duration(u.Filters.ExternalAuthCacheTime) * time.Second
if cacheTime > delay {
delay = cacheTime
}
}
if isLastActivityRecent(u.LastLogin, delay) {
if u.LastLogin > u.UpdatedAt {
if config.IsShared == 1 {
u.checkLocalHomeDir(connectionID)
}
return nil
}
}
err := u.checkRootPath(connectionID)
if err != nil {
return err
}
if u.Filters.StartDirectory != "" {
err = u.checkDirWithParents(u.Filters.StartDirectory, connectionID)
if err != nil {
logger.Warn(logSender, connectionID, "could not create start directory %q, err: %v",
u.Filters.StartDirectory, err)
}
}
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
fs, err := u.GetFilesystemForPath(v.VirtualPath, connectionID)
if err == nil {
fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
}
// now check intermediary folders
err = u.checkDirWithParents(path.Dir(v.VirtualPath), connectionID)
if err != nil {
logger.Warn(logSender, connectionID, "could not create intermediary dir to %q, err: %v", v.VirtualPath, err)
}
}
return nil
}
// GetCleanedPath returns a clean POSIX absolute path using the user start directory as base
// if the provided rawVirtualPath is relative
func (u *User) GetCleanedPath(rawVirtualPath string) string {
if u.Filters.StartDirectory != "" {
if !path.IsAbs(rawVirtualPath) {
var b strings.Builder
b.Grow(len(u.Filters.StartDirectory) + 1 + len(rawVirtualPath))
b.WriteString(u.Filters.StartDirectory)
b.WriteString("/")
b.WriteString(rawVirtualPath)
return util.CleanPath(b.String())
}
}
return util.CleanPath(rawVirtualPath)
}
// isFsEqual returns true if the filesystem configurations are the same
func (u *User) isFsEqual(other *User) bool {
if u.FsConfig.Provider == sdk.LocalFilesystemProvider && u.GetHomeDir() != other.GetHomeDir() {
return false
}
if !u.FsConfig.IsEqual(other.FsConfig) {
return false
}
if u.Filters.StartDirectory != other.Filters.StartDirectory {
return false
}
if len(u.VirtualFolders) != len(other.VirtualFolders) {
return false
}
for idx := range u.VirtualFolders {
f := &u.VirtualFolders[idx]
found := false
for idx1 := range other.VirtualFolders {
f1 := &other.VirtualFolders[idx1]
if f.VirtualPath == f1.VirtualPath {
found = true
if f.FsConfig.Provider == sdk.LocalFilesystemProvider && f.MappedPath != f1.MappedPath {
return false
}
if !f.FsConfig.IsEqual(f1.FsConfig) {
return false
}
}
}
if !found {
return false
}
}
return true
}
// CheckLoginConditions checks if the user is active and not expired
func (u *User) CheckLoginConditions() error {
if u.Status < 1 {
return fmt.Errorf("user %q is disabled", u.Username)
}
if u.ExpirationDate > 0 && u.ExpirationDate < util.GetTimeAsMsSinceEpoch(time.Now()) {
return fmt.Errorf("user %q is expired, expiration timestamp: %v current timestamp: %v", u.Username,
u.ExpirationDate, util.GetTimeAsMsSinceEpoch(time.Now()))
}
return nil
}
// hideConfidentialData hides user confidential data
func (u *User) hideConfidentialData() {
u.Password = ""
u.FsConfig.HideConfidentialData()
if u.Filters.TOTPConfig.Secret != nil {
u.Filters.TOTPConfig.Secret.Hide()
}
for _, code := range u.Filters.RecoveryCodes {
if code.Secret != nil {
code.Secret.Hide()
}
}
}
// GetSubDirPermissions returns permissions for sub directories
func (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {
var result []sdk.DirectoryPermissions
for k, v := range u.Permissions {
if k == "/" {
continue
}
dirPerms := sdk.DirectoryPermissions{
Path: k,
Permissions: v,
}
result = append(result, dirPerms)
}
return result
}
func (u *User) setAnonymousSettings() {
for k := range u.Permissions {
u.Permissions[k] = []string{PermListItems, PermDownload}
}
u.Filters.DeniedProtocols = append(u.Filters.DeniedProtocols, protocolSSH, protocolHTTP)
u.Filters.DeniedProtocols = util.RemoveDuplicates(u.Filters.DeniedProtocols, false)
for _, method := range ValidLoginMethods {
if method != LoginMethodPassword {
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, method)
}
}
u.Filters.DeniedLoginMethods = util.RemoveDuplicates(u.Filters.DeniedLoginMethods, false)
}
// RenderAsJSON implements the renderer interface used within plugins
func (u *User) RenderAsJSON(reload bool) ([]byte, error) {
if reload {
user, err := provider.userExists(u.Username, "")
if err != nil {
providerLog(logger.LevelError, "unable to reload user before rendering as json: %v", err)
return nil, err
}
user.PrepareForRendering()
return json.Marshal(user)
}
u.PrepareForRendering()
return json.Marshal(u)
}
// PrepareForRendering prepares a user for rendering.
// It hides confidential data and set to nil the empty secrets
// so they are not serialized
func (u *User) PrepareForRendering() {
u.hideConfidentialData()
u.FsConfig.SetNilSecretsIfEmpty()
for idx := range u.VirtualFolders {
folder := &u.VirtualFolders[idx]
folder.PrepareForRendering()
}
}
// HasRedactedSecret returns true if the user has a redacted secret
func (u *User) hasRedactedSecret() bool {
if u.FsConfig.HasRedactedSecret() {
return true
}
for idx := range u.VirtualFolders {
folder := &u.VirtualFolders[idx]
if folder.HasRedactedSecret() {
return true
}
}
return u.Filters.TOTPConfig.Secret.IsRedacted()
}
// CloseFs closes the underlying filesystems
func (u *User) CloseFs() error {
if u.fsCache == nil {
return nil
}
var err error
for _, fs := range u.fsCache {
errClose := fs.Close()
if err == nil {
err = errClose
}
}
return err
}
// IsPasswordHashed returns true if the password is hashed
func (u *User) IsPasswordHashed() bool {
return util.IsStringPrefixInSlice(u.Password, hashPwdPrefixes)
}
// IsTLSUsernameVerificationEnabled returns true if we need to extract the username
// from the client TLS certificate
func (u *User) IsTLSUsernameVerificationEnabled() bool {
if u.Filters.TLSUsername != "" {
return u.Filters.TLSUsername != sdk.TLSUsernameNone
}
return false
}
// SetEmptySecrets sets to empty any user secret
func (u *User) SetEmptySecrets() {
u.FsConfig.SetEmptySecrets()
for idx := range u.VirtualFolders {
folder := &u.VirtualFolders[idx]
folder.FsConfig.SetEmptySecrets()
}
u.Filters.TOTPConfig.Secret = kms.NewEmptySecret()
}
// GetPermissionsForPath returns the permissions for the given path.
// The path must be a SFTPGo virtual path
func (u *User) GetPermissionsForPath(p string) []string {
permissions := []string{}
if perms, ok := u.Permissions["/"]; ok {
// if only root permissions are defined returns them unconditionally
if len(u.Permissions) == 1 {
return perms
}
// fallback permissions
permissions = perms
}
dirsForPath := util.GetDirsForVirtualPath(p)
// dirsForPath contains all the dirs for a given path in reverse order
// for example if the path is: /1/2/3/4 it contains:
// [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ]
// so the first match is the one we are interested to
for idx := range dirsForPath {
if perms, ok := u.Permissions[dirsForPath[idx]]; ok {
return perms
}
for dir, perms := range u.Permissions {
if match, err := path.Match(dir, dirsForPath[idx]); err == nil && match {
return perms
}
}
}
return permissions
}
func (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {
if allowSelfConnections == 0 {
return nil, nil
}
sftpUser, err := UserExists(username, "")
if err == nil {
err = sftpUser.LoadAndApplyGroupSettings()
}
if err == nil {
// we don't allow local nested SFTP folders
var forbiddens []string
if sftpUser.FsConfig.Provider == sdk.SFTPFilesystemProvider {
forbiddens = append(forbiddens, sftpUser.Username)
return forbiddens, nil
}
for idx := range sftpUser.VirtualFolders {
v := &sftpUser.VirtualFolders[idx]
if v.FsConfig.Provider == sdk.SFTPFilesystemProvider {
forbiddens = append(forbiddens, sftpUser.Username)
return forbiddens, nil
}
}
return forbiddens, nil
}
if !errors.Is(err, util.ErrNotFound) {
return nil, err
}
return nil, nil
}
// GetFsConfigForPath returns the file system configuration for the specified virtual path
func (u *User) GetFsConfigForPath(virtualPath string) vfs.Filesystem {
if virtualPath != "" && virtualPath != "/" && len(u.VirtualFolders) > 0 {
folder, err := u.GetVirtualFolderForPath(virtualPath)
if err == nil {
return folder.FsConfig
}
}
return u.FsConfig
}
// GetFilesystemForPath returns the filesystem for the given path
func (u *User) GetFilesystemForPath(virtualPath, connectionID string) (vfs.Fs, error) {
if u.fsCache == nil {
u.fsCache = make(map[string]vfs.Fs)
}
// allow to override the `/` path with a virtual folder
if len(u.VirtualFolders) > 0 {
folder, err := u.GetVirtualFolderForPath(virtualPath)
if err == nil {
if fs, ok := u.fsCache[folder.VirtualPath]; ok {
return fs, nil
}
forbiddenSelfUsers := []string{u.Username}
if folder.FsConfig.Provider == sdk.SFTPFilesystemProvider {
forbiddens, err := u.getForbiddenSFTPSelfUsers(folder.FsConfig.SFTPConfig.Username)
if err != nil {
return nil, err
}
forbiddenSelfUsers = append(forbiddenSelfUsers, forbiddens...)
}
fs, err := folder.GetFilesystem(connectionID, forbiddenSelfUsers)
if err == nil {
u.fsCache[folder.VirtualPath] = fs
}
return fs, err
}
}
if val, ok := u.fsCache["/"]; ok {
return val, nil
}
fs, err := u.getRootFs(connectionID)
if err != nil {
return fs, err
}
u.fsCache["/"] = fs
return fs, err
}
// GetVirtualFolderForPath returns the virtual folder containing the specified virtual path.
// If the path is not inside a virtual folder an error is returned
func (u *User) GetVirtualFolderForPath(virtualPath string) (vfs.VirtualFolder, error) {
var folder vfs.VirtualFolder
if len(u.VirtualFolders) == 0 {
return folder, errNoMatchingVirtualFolder
}
dirsForPath := util.GetDirsForVirtualPath(virtualPath)
for index := range dirsForPath {
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
if v.VirtualPath == dirsForPath[index] {
return *v, nil
}
}
}
return folder, errNoMatchingVirtualFolder
}
// CheckMetadataConsistency checks the consistency between the metadata stored
// in the configured metadata plugin and the filesystem
func (u *User) CheckMetadataConsistency() error {
fs, err := u.getRootFs(xid.New().String())
if err != nil {
return err
}
defer fs.Close()
if err = fs.CheckMetadata(); err != nil {
return err
}
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
if err = v.CheckMetadataConsistency(); err != nil {
return err
}
}
return nil
}
// ScanQuota scans the user home dir and virtual folders, included in its quota,
// and returns the number of files and their size
func (u *User) ScanQuota() (int, int64, error) {
fs, err := u.getRootFs(xid.New().String())
if err != nil {
return 0, 0, err
}
defer fs.Close()
numFiles, size, err := fs.ScanRootDirContents()
if err != nil {
return numFiles, size, err
}
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
if !v.IsIncludedInUserQuota() {
continue
}
num, s, err := v.ScanQuota()
if err != nil {
return numFiles, size, err
}
numFiles += num
size += s
}
return numFiles, size, nil
}
// GetVirtualFoldersInPath returns the virtual folders inside virtualPath including
// any parents
func (u *User) GetVirtualFoldersInPath(virtualPath string) map[string]bool {
result := make(map[string]bool)
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
dirsForPath := util.GetDirsForVirtualPath(v.VirtualPath)
for index := range dirsForPath {
d := dirsForPath[index]
if d == "/" {
continue
}
if path.Dir(d) == virtualPath {
result[d] = true
}
}
}
if u.Filters.StartDirectory != "" {
dirsForPath := util.GetDirsForVirtualPath(u.Filters.StartDirectory)
for index := range dirsForPath {
d := dirsForPath[index]
if d == "/" {
continue
}
if path.Dir(d) == virtualPath {
result[d] = true
}
}
}
return result
}
func (u *User) hasVirtualDirs() bool {
if u.Filters.StartDirectory != "" {
return true
}
numFolders := len(u.VirtualFolders)
if numFolders == 1 {
return u.VirtualFolders[0].VirtualPath != "/"
}
return numFolders > 0
}
// FilterListDir adds virtual folders and remove hidden items from the given files list
func (u *User) FilterListDir(dirContents []os.FileInfo, virtualPath string) []os.FileInfo {
filter := u.getPatternsFilterForPath(virtualPath)
if !u.hasVirtualDirs() && filter.DenyPolicy != sdk.DenyPolicyHide {
return dirContents
}
vdirs := make(map[string]bool)
for dir := range u.GetVirtualFoldersInPath(virtualPath) {
dirName := path.Base(dir)
if filter.DenyPolicy == sdk.DenyPolicyHide {
if !filter.CheckAllowed(dirName) {
continue
}
}
vdirs[dirName] = true
}
validIdx := 0
for index, fi := range dirContents {
for dir := range vdirs {
if fi.Name() == dir {
if !fi.IsDir() {
fi = vfs.NewFileInfo(dir, true, 0, time.Unix(0, 0), false)
dirContents[index] = fi
}
delete(vdirs, dir)
}
}
if filter.DenyPolicy == sdk.DenyPolicyHide {
if filter.CheckAllowed(fi.Name()) {
dirContents[validIdx] = fi
validIdx++
}
}
}
if filter.DenyPolicy == sdk.DenyPolicyHide {
for idx := validIdx; idx < len(dirContents); idx++ {
dirContents[idx] = nil
}
dirContents = dirContents[:validIdx]
}
for dir := range vdirs {
fi := vfs.NewFileInfo(dir, true, 0, time.Unix(0, 0), false)
dirContents = append(dirContents, fi)
}
return dirContents
}
// IsMappedPath returns true if the specified filesystem path has a virtual folder mapping.
// The filesystem path must be cleaned before calling this method
func (u *User) IsMappedPath(fsPath string) bool {
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
if fsPath == v.MappedPath {
return true
}
}
return false
}
// IsVirtualFolder returns true if the specified virtual path is a virtual folder
func (u *User) IsVirtualFolder(virtualPath string) bool {
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
if virtualPath == v.VirtualPath {
return true
}
}
return false
}
// HasVirtualFoldersInside returns true if there are virtual folders inside the
// specified virtual path. We assume that path are cleaned
func (u *User) HasVirtualFoldersInside(virtualPath string) bool {
if virtualPath == "/" && len(u.VirtualFolders) > 0 {
return true
}
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
if len(v.VirtualPath) > len(virtualPath) {
if strings.HasPrefix(v.VirtualPath, virtualPath+"/") {
return true
}
}
}
return false
}
// HasPermissionsInside returns true if the specified virtualPath has no permissions itself and
// no subdirs with defined permissions
func (u *User) HasPermissionsInside(virtualPath string) bool {
for dir, perms := range u.Permissions {
if len(perms) == 1 && perms[0] == PermAny {
continue
}
if dir == virtualPath {
return true
} else if len(dir) > len(virtualPath) {
if strings.HasPrefix(dir, virtualPath+"/") {
return true
}
}
}
return false
}
// HasPerm returns true if the user has the given permission or any permission
func (u *User) HasPerm(permission, path string) bool {
perms := u.GetPermissionsForPath(path)
if util.Contains(perms, PermAny) {
return true
}
return util.Contains(perms, permission)
}
// HasAnyPerm returns true if the user has at least one of the given permissions
func (u *User) HasAnyPerm(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if util.Contains(perms, PermAny) {
return true
}
for _, permission := range permissions {
if util.Contains(perms, permission) {
return true
}
}
return false
}
// HasPerms returns true if the user has all the given permissions
func (u *User) HasPerms(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if util.Contains(perms, PermAny) {
return true
}
for _, permission := range permissions {
if !util.Contains(perms, permission) {
return false
}
}
return true
}
// HasPermsDeleteAll returns true if the user can delete both files and directories
// for the given path
func (u *User) HasPermsDeleteAll(path string) bool {
perms := u.GetPermissionsForPath(path)
canDeleteFiles := false
canDeleteDirs := false
for _, permission := range perms {
if permission == PermAny || permission == PermDelete {
return true
}
if permission == PermDeleteFiles {
canDeleteFiles = true
}
if permission == PermDeleteDirs {
canDeleteDirs = true
}
}
return canDeleteFiles && canDeleteDirs
}
// HasPermsRenameAll returns true if the user can rename both files and directories
// for the given path
func (u *User) HasPermsRenameAll(path string) bool {
perms := u.GetPermissionsForPath(path)
canRenameFiles := false
canRenameDirs := false
for _, permission := range perms {
if permission == PermAny || permission == PermRename {
return true
}
if permission == PermRenameFiles {
canRenameFiles = true
}
if permission == PermRenameDirs {
canRenameDirs = true
}
}
return canRenameFiles && canRenameDirs
}
// HasNoQuotaRestrictions returns true if no quota restrictions need to be applyed
func (u *User) HasNoQuotaRestrictions(checkFiles bool) bool {
if u.QuotaSize == 0 && (!checkFiles || u.QuotaFiles == 0) {
return true
}
return false
}
// IsLoginMethodAllowed returns true if the specified login method is allowed
func (u *User) IsLoginMethodAllowed(loginMethod, protocol string, partialSuccessMethods []string) bool {
if len(u.Filters.DeniedLoginMethods) == 0 {
return true
}
if len(partialSuccessMethods) == 1 {
for _, method := range u.GetNextAuthMethods(partialSuccessMethods, true) {
if method == loginMethod {
return true
}
}
}
if util.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
return false
}
if protocol == protocolSSH && loginMethod == LoginMethodPassword {
if util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
return false
}
}
return true
}
// GetNextAuthMethods returns the list of authentications methods that
// can continue for multi-step authentication
func (u *User) GetNextAuthMethods(partialSuccessMethods []string, isPasswordAuthEnabled bool) []string {
var methods []string
if len(partialSuccessMethods) != 1 {
return methods
}
if partialSuccessMethods[0] != SSHLoginMethodPublicKey {
return methods
}
for _, method := range u.GetAllowedLoginMethods() {
if method == SSHLoginMethodKeyAndPassword && isPasswordAuthEnabled {
methods = append(methods, LoginMethodPassword)
}
if method == SSHLoginMethodKeyAndKeyboardInt {
methods = append(methods, SSHLoginMethodKeyboardInteractive)
}
}
return methods
}
// IsPartialAuth returns true if the specified login method is a step for
// a multi-step Authentication.
// We support publickey+password and publickey+keyboard-interactive, so
// only publickey can returns partial success.
// We can have partial success if only multi-step Auth methods are enabled
func (u *User) IsPartialAuth(loginMethod string) bool {
if loginMethod != SSHLoginMethodPublicKey {
return false
}
for _, method := range u.GetAllowedLoginMethods() {
if method == LoginMethodTLSCertificate || method == LoginMethodTLSCertificateAndPwd ||
method == SSHLoginMethodPassword {
continue
}
if method == LoginMethodPassword && util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
continue
}
if !util.Contains(SSHMultiStepsLoginMethods, method) {
return false
}
}
return true
}
// GetAllowedLoginMethods returns the allowed login methods
func (u *User) GetAllowedLoginMethods() []string {
var allowedMethods []string
for _, method := range ValidLoginMethods {
if method == SSHLoginMethodPassword {
continue
}
if !util.Contains(u.Filters.DeniedLoginMethods, method) {
allowedMethods = append(allowedMethods, method)
}
}
return allowedMethods
}
func (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {
var filter sdk.PatternsFilter
if len(u.Filters.FilePatterns) == 0 {
return filter
}
dirsForPath := util.GetDirsForVirtualPath(virtualPath)
for _, dir := range dirsForPath {
for _, f := range u.Filters.FilePatterns {
if f.Path == dir {
filter = f
break
}
}
if filter.Path != "" {
break
}
}
return filter
}
func (u *User) isDirHidden(virtualPath string) bool {
if len(u.Filters.FilePatterns) == 0 {
return false
}
for _, dirPath := range util.GetDirsForVirtualPath(virtualPath) {
if dirPath == "/" {
return false
}
filter := u.getPatternsFilterForPath(dirPath)
if filter.DenyPolicy == sdk.DenyPolicyHide {
if !filter.CheckAllowed(path.Base(dirPath)) {
return true
}
}
}
return false
}
func (u *User) getMinPasswordEntropy() float64 {
if u.Filters.PasswordStrength > 0 {
return float64(u.Filters.PasswordStrength)
}
return config.PasswordValidation.Users.MinEntropy
}
// IsFileAllowed returns true if the specified file is allowed by the file restrictions filters.
// The second parameter returned is the deny policy
func (u *User) IsFileAllowed(virtualPath string) (bool, int) {
dirPath := path.Dir(virtualPath)
if u.isDirHidden(dirPath) {
return false, sdk.DenyPolicyHide
}
filter := u.getPatternsFilterForPath(dirPath)
return filter.CheckAllowed(path.Base(virtualPath)), filter.DenyPolicy
}
// CanManageMFA returns true if the user can add a multi-factor authentication configuration
func (u *User) CanManageMFA() bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
return false
}
return len(mfa.GetAvailableTOTPConfigs()) > 0
}
func (u *User) isExternalAuthCached() bool {
if u.ID <= 0 {
return false
}
if u.Filters.ExternalAuthCacheTime <= 0 {
return false
}
return isLastActivityRecent(u.LastLogin, time.Duration(u.Filters.ExternalAuthCacheTime)*time.Second)
}
// CanManageShares returns true if the user can add, update and list shares
func (u *User) CanManageShares() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
}
// CanResetPassword returns true if this user is allowed to reset its password
func (u *User) CanResetPassword() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
}
// CanChangePassword returns true if this user is allowed to change its password
func (u *User) CanChangePassword() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
}
// CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
func (u *User) CanChangeAPIKeyAuth() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
}
// CanChangeInfo returns true if this user is allowed to change its info such as email and description
func (u *User) CanChangeInfo() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
}
// CanManagePublicKeys returns true if this user is allowed to manage public keys
// from the web client. Used in web client UI
func (u *User) CanManagePublicKeys() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
}
// CanAddFilesFromWeb returns true if the client can add files from the web UI.
// The specified target is the directory where the files must be uploaded
func (u *User) CanAddFilesFromWeb(target string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)
}
// CanAddDirsFromWeb returns true if the client can add directories from the web UI.
// The specified target is the directory where the new directory must be created
func (u *User) CanAddDirsFromWeb(target string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasPerm(PermCreateDirs, target)
}
// CanRenameFromWeb returns true if the client can rename objects from the web UI.
// The specified src and dest are the source and target directories for the rename.
func (u *User) CanRenameFromWeb(src, dest string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)
}
// CanDeleteFromWeb returns true if the client can delete objects from the web UI.
// The specified target is the parent directory for the object to delete
func (u *User) CanDeleteFromWeb(target string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasAnyPerm(permsDeleteAny, target)
}
// PasswordExpiresIn returns the number of days before the password expires.
// The returned value is negative if the password is expired.
// The caller must ensure that a PasswordExpiration is set
func (u *User) PasswordExpiresIn() int {
lastPwdChange := util.GetTimeFromMsecSinceEpoch(u.LastPasswordChange)
pwdExpiration := lastPwdChange.Add(time.Duration(u.Filters.PasswordExpiration) * 24 * time.Hour)
res := int(math.Round(float64(time.Until(pwdExpiration)) / float64(24*time.Hour)))
if res == 0 && pwdExpiration.After(time.Now()) {
res = 1
}
return res
}
// MustChangePassword returns true if the user must change the password
func (u *User) MustChangePassword() bool {
if u.Filters.RequirePasswordChange {
return true
}
if u.Filters.PasswordExpiration == 0 {
return false
}
lastPwdChange := util.GetTimeFromMsecSinceEpoch(u.LastPasswordChange)
return lastPwdChange.Add(time.Duration(u.Filters.PasswordExpiration) * 24 * time.Hour).Before(time.Now())
}
// MustSetSecondFactor returns true if the user must set a second factor authentication
func (u *User) MustSetSecondFactor() bool {
if len(u.Filters.TwoFactorAuthProtocols) > 0 {
if !u.Filters.TOTPConfig.Enabled {
return true
}
for _, p := range u.Filters.TwoFactorAuthProtocols {
if !util.Contains(u.Filters.TOTPConfig.Protocols, p) {
return true
}
}
}
return false
}
// MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
// for the specified protocol
func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
if util.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
if !u.Filters.TOTPConfig.Enabled {
return true
}
if !util.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
return true
}
}
return false
}
// GetSignature returns a signature for this admin.
// It will change after an update
func (u *User) GetSignature() string {
return strconv.FormatInt(u.UpdatedAt, 10)
}
// GetBandwidthForIP returns the upload and download bandwidth for the specified IP
func (u *User) GetBandwidthForIP(clientIP, connectionID string) (int64, int64) {
if len(u.Filters.BandwidthLimits) > 0 {
ip := net.ParseIP(clientIP)
if ip != nil {
for _, bwLimit := range u.Filters.BandwidthLimits {
for _, source := range bwLimit.Sources {
_, ipNet, err := net.ParseCIDR(source)
if err == nil {
if ipNet.Contains(ip) {
logger.Debug(logSender, connectionID, "override bandwidth limit for ip %q, upload limit: %v KB/s, download limit: %v KB/s",
clientIP, bwLimit.UploadBandwidth, bwLimit.DownloadBandwidth)
return bwLimit.UploadBandwidth, bwLimit.DownloadBandwidth
}
}
}
}
}
}
return u.UploadBandwidth, u.DownloadBandwidth
}
// IsLoginFromAddrAllowed returns true if the login is allowed from the specified remoteAddr.
// If AllowedIP is defined only the specified IP/Mask can login.
// If DeniedIP is defined the specified IP/Mask cannot login.
// If an IP is both allowed and denied then login will be allowed
func (u *User) IsLoginFromAddrAllowed(remoteAddr string) bool {
if len(u.Filters.AllowedIP) == 0 && len(u.Filters.DeniedIP) == 0 {
return true
}
remoteIP := net.ParseIP(util.GetIPFromRemoteAddress(remoteAddr))
// if remoteIP is invalid we allow login, this should never happen
if remoteIP == nil {
logger.Warn(logSender, "", "login allowed for invalid IP. remote address: %q", remoteAddr)
return true
}
for _, IPMask := range u.Filters.AllowedIP {
_, IPNet, err := net.ParseCIDR(IPMask)
if err != nil {
return false
}
if IPNet.Contains(remoteIP) {
return true
}
}
for _, IPMask := range u.Filters.DeniedIP {
_, IPNet, err := net.ParseCIDR(IPMask)
if err != nil {
return false
}
if IPNet.Contains(remoteIP) {
return false
}
}
return len(u.Filters.AllowedIP) == 0
}
// GetPermissionsAsJSON returns the permissions as json byte array
func (u *User) GetPermissionsAsJSON() ([]byte, error) {
return json.Marshal(u.Permissions)
}
// GetPublicKeysAsJSON returns the public keys as json byte array
func (u *User) GetPublicKeysAsJSON() ([]byte, error) {
return json.Marshal(u.PublicKeys)
}
// GetFiltersAsJSON returns the filters as json byte array
func (u *User) GetFiltersAsJSON() ([]byte, error) {
return json.Marshal(u.Filters)
}
// GetFsConfigAsJSON returns the filesystem config as json byte array
func (u *User) GetFsConfigAsJSON() ([]byte, error) {
return json.Marshal(u.FsConfig)
}
// GetUID returns a validate uid, suitable for use with os.Chown
func (u *User) GetUID() int {
if u.UID <= 0 || u.UID > math.MaxInt32 {
return -1
}
return u.UID
}
// GetGID returns a validate gid, suitable for use with os.Chown
func (u *User) GetGID() int {
if u.GID <= 0 || u.GID > math.MaxInt32 {
return -1
}
return u.GID
}
// GetHomeDir returns the shortest path name equivalent to the user's home directory
func (u *User) GetHomeDir() string {
return u.HomeDir
}
// HasRecentActivity returns true if the last user login is recent and so we can skip some expensive checks
func (u *User) HasRecentActivity() bool {
return isLastActivityRecent(u.LastLogin, lastLoginMinDelay)
}
// HasQuotaRestrictions returns true if there are any disk quota restrictions
func (u *User) HasQuotaRestrictions() bool {
return u.QuotaFiles > 0 || u.QuotaSize > 0
}
// HasTransferQuotaRestrictions returns true if there are any data transfer restrictions
func (u *User) HasTransferQuotaRestrictions() bool {
if len(u.Filters.DataTransferLimits) > 0 {
return true
}
return u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0
}
// GetDataTransferLimits returns upload, download and total data transfer limits
func (u *User) GetDataTransferLimits(clientIP string) (int64, int64, int64) {
var total, ul, dl int64
if len(u.Filters.DataTransferLimits) > 0 {
ip := net.ParseIP(clientIP)
if ip != nil {
for _, limit := range u.Filters.DataTransferLimits {
for _, source := range limit.Sources {
_, ipNet, err := net.ParseCIDR(source)
if err == nil {
if ipNet.Contains(ip) {
if limit.TotalDataTransfer > 0 {
total = limit.TotalDataTransfer * 1048576
}
if limit.DownloadDataTransfer > 0 {
dl = limit.DownloadDataTransfer * 1048576
}
if limit.UploadDataTransfer > 0 {
ul = limit.UploadDataTransfer * 1048576
}
return ul, dl, total
}
}
}
}
}
}
if u.TotalDataTransfer > 0 {
total = u.TotalDataTransfer * 1048576
}
if u.DownloadDataTransfer > 0 {
dl = u.DownloadDataTransfer * 1048576
}
if u.UploadDataTransfer > 0 {
ul = u.UploadDataTransfer * 1048576
}
return ul, dl, total
}
// GetQuotaSummary returns used quota and limits if defined
func (u *User) GetQuotaSummary() string {
var sb strings.Builder
addSection := func() {
if sb.Len() > 0 {
sb.WriteString(". ")
}
}
if u.UsedQuotaFiles > 0 || u.QuotaFiles > 0 {
sb.WriteString(fmt.Sprintf("Files: %v", u.UsedQuotaFiles))
if u.QuotaFiles > 0 {
sb.WriteString(fmt.Sprintf("/%v", u.QuotaFiles))
}
}
if u.UsedQuotaSize > 0 || u.QuotaSize > 0 {
addSection()
sb.WriteString(fmt.Sprintf("Size: %v", util.ByteCountIEC(u.UsedQuotaSize)))
if u.QuotaSize > 0 {
sb.WriteString(fmt.Sprintf("/%v", util.ByteCountIEC(u.QuotaSize)))
}
}
if u.TotalDataTransfer > 0 {
addSection()
total := u.UsedDownloadDataTransfer + u.UsedUploadDataTransfer
sb.WriteString(fmt.Sprintf("Transfer: %v/%v", util.ByteCountIEC(total),
util.ByteCountIEC(u.TotalDataTransfer*1048576)))
}
if u.UploadDataTransfer > 0 {
addSection()
sb.WriteString(fmt.Sprintf("UL: %v/%v", util.ByteCountIEC(u.UsedUploadDataTransfer),
util.ByteCountIEC(u.UploadDataTransfer*1048576)))
}
if u.DownloadDataTransfer > 0 {
addSection()
sb.WriteString(fmt.Sprintf("DL: %v/%v", util.ByteCountIEC(u.UsedDownloadDataTransfer),
util.ByteCountIEC(u.DownloadDataTransfer*1048576)))
}
return sb.String()
}
// GetPermissionsAsString returns the user's permissions as comma separated string
func (u *User) GetPermissionsAsString() string {
result := ""
for dir, perms := range u.Permissions {
dirPerms := strings.Join(perms, ", ")
dp := fmt.Sprintf("%q: %q", dir, dirPerms)
if dir == "/" {
if result != "" {
result = dp + ", " + result
} else {
result = dp
}
} else {
if result != "" {
result += ", "
}
result += dp
}
}
return result
}
// GetBandwidthAsString returns bandwidth limits if defines
func (u *User) GetBandwidthAsString() string {
var sb strings.Builder
sb.WriteString("DL: ")
if u.DownloadBandwidth > 0 {
sb.WriteString(util.ByteCountIEC(u.DownloadBandwidth*1000) + "/s.")
} else {
sb.WriteString("unlimited.")
}
sb.WriteString(" UL: ")
if u.UploadBandwidth > 0 {
sb.WriteString(util.ByteCountIEC(u.UploadBandwidth*1000) + "/s.")
} else {
sb.WriteString("unlimited.")
}
return sb.String()
}
// GetMFAStatusAsString returns MFA status
func (u *User) GetMFAStatusAsString() string {
if u.Filters.TOTPConfig.Enabled {
return strings.Join(u.Filters.TOTPConfig.Protocols, ", ")
}
return "Disabled"
}
// GetLastLoginAsString returns the last login as string
func (u *User) GetLastLoginAsString() string {
if u.LastLogin > 0 {
return util.GetTimeFromMsecSinceEpoch(u.LastLogin).UTC().Format(iso8601UTCFormat)
}
return ""
}
// GetLastQuotaUpdateAsString returns the last quota update as string
func (u *User) GetLastQuotaUpdateAsString() string {
if u.LastQuotaUpdate > 0 {
return util.GetTimeFromMsecSinceEpoch(u.LastQuotaUpdate).UTC().Format(iso8601UTCFormat)
}
return ""
}
// GetStorageDescrition returns the storage description
func (u *User) GetStorageDescrition() string {
switch u.FsConfig.Provider {
case sdk.LocalFilesystemProvider:
return fmt.Sprintf("Local: %v", u.GetHomeDir())
case sdk.S3FilesystemProvider:
return fmt.Sprintf("S3: %v", u.FsConfig.S3Config.Bucket)
case sdk.GCSFilesystemProvider:
return fmt.Sprintf("GCS: %v", u.FsConfig.GCSConfig.Bucket)
case sdk.AzureBlobFilesystemProvider:
return fmt.Sprintf("AzBlob: %v", u.FsConfig.AzBlobConfig.Container)
case sdk.CryptedFilesystemProvider:
return fmt.Sprintf("Encrypted: %v", u.GetHomeDir())
case sdk.SFTPFilesystemProvider:
return fmt.Sprintf("SFTP: %v", u.FsConfig.SFTPConfig.Endpoint)
case sdk.HTTPFilesystemProvider:
return fmt.Sprintf("HTTP: %v", u.FsConfig.HTTPConfig.Endpoint)
default:
return ""
}
}
// GetGroupsAsString returns the user's groups as a string
func (u *User) GetGroupsAsString() string {
if len(u.Groups) == 0 {
return ""
}
var groups []string
for _, g := range u.Groups {
if g.Type == sdk.GroupTypePrimary {
groups = append(groups, "")
copy(groups[1:], groups)
groups[0] = g.Name
} else {
groups = append(groups, g.Name)
}
}
return strings.Join(groups, ",")
}
// GetInfoString returns user's info as string.
// Storage provider, number of public keys, max sessions, uid,
// gid, denied and allowed IP/Mask are returned
func (u *User) GetInfoString() string {
var result strings.Builder
if len(u.PublicKeys) > 0 {
result.WriteString(fmt.Sprintf("Public keys: %v. ", len(u.PublicKeys)))
}
if u.MaxSessions > 0 {
result.WriteString(fmt.Sprintf("Max sessions: %v. ", u.MaxSessions))
}
if u.UID > 0 {
result.WriteString(fmt.Sprintf("UID: %v. ", u.UID))
}
if u.GID > 0 {
result.WriteString(fmt.Sprintf("GID: %v. ", u.GID))
}
if len(u.Filters.DeniedLoginMethods) > 0 {
result.WriteString(fmt.Sprintf("Denied login methods: %v. ", strings.Join(u.Filters.DeniedLoginMethods, ",")))
}
if len(u.Filters.DeniedProtocols) > 0 {
result.WriteString(fmt.Sprintf("Denied protocols: %v. ", strings.Join(u.Filters.DeniedProtocols, ",")))
}
if len(u.Filters.DeniedIP) > 0 {
result.WriteString(fmt.Sprintf("Denied IP/Mask: %v. ", len(u.Filters.DeniedIP)))
}
if len(u.Filters.AllowedIP) > 0 {
result.WriteString(fmt.Sprintf("Allowed IP/Mask: %v", len(u.Filters.AllowedIP)))
}
return result.String()
}
// GetStatusAsString returns the user status as a string
func (u *User) GetStatusAsString() string {
if u.ExpirationDate > 0 && u.ExpirationDate < util.GetTimeAsMsSinceEpoch(time.Now()) {
return "Expired"
}
if u.Status == 1 {
return "Active"
}
return "Inactive"
}
// GetExpirationDateAsString returns expiration date formatted as YYYY-MM-DD
func (u *User) GetExpirationDateAsString() string {
if u.ExpirationDate > 0 {
t := util.GetTimeFromMsecSinceEpoch(u.ExpirationDate)
return t.Format("2006-01-02")
}
return ""
}
// GetAllowedIPAsString returns the allowed IP as comma separated string
func (u *User) GetAllowedIPAsString() string {
return strings.Join(u.Filters.AllowedIP, ",")
}
// GetDeniedIPAsString returns the denied IP as comma separated string
func (u *User) GetDeniedIPAsString() string {
return strings.Join(u.Filters.DeniedIP, ",")
}
// HasExternalAuth returns true if the external authentication is globally enabled
// and it is not disabled for this user
func (u *User) HasExternalAuth() bool {
if u.Filters.Hooks.ExternalAuthDisabled {
return false
}
if config.ExternalAuthHook != "" {
return true
}
return plugin.Handler.HasAuthenticators()
}
// CountUnusedRecoveryCodes returns the number of unused recovery codes
func (u *User) CountUnusedRecoveryCodes() int {
unused := 0
for _, code := range u.Filters.RecoveryCodes {
if !code.Used {
unused++
}
}
return unused
}
// SetEmptySecretsIfNil sets the secrets to empty if nil
func (u *User) SetEmptySecretsIfNil() {
u.HasPassword = u.Password != ""
u.FsConfig.SetEmptySecretsIfNil()
for idx := range u.VirtualFolders {
vfolder := &u.VirtualFolders[idx]
vfolder.FsConfig.SetEmptySecretsIfNil()
}
if u.Filters.TOTPConfig.Secret == nil {
u.Filters.TOTPConfig.Secret = kms.NewEmptySecret()
}
}
func (u *User) hasMainDataTransferLimits() bool {
return u.UploadDataTransfer > 0 || u.DownloadDataTransfer > 0 || u.TotalDataTransfer > 0
}
// HasPrimaryGroup returns true if the user has the specified primary group
func (u *User) HasPrimaryGroup(name string) bool {
for _, g := range u.Groups {
if g.Name == name {
return g.Type == sdk.GroupTypePrimary
}
}
return false
}
// HasSecondaryGroup returns true if the user has the specified secondary group
func (u *User) HasSecondaryGroup(name string) bool {
for _, g := range u.Groups {
if g.Name == name {
return g.Type == sdk.GroupTypeSecondary
}
}
return false
}
// HasMembershipGroup returns true if the user has the specified membership group
func (u *User) HasMembershipGroup(name string) bool {
for _, g := range u.Groups {
if g.Name == name {
return g.Type == sdk.GroupTypeMembership
}
}
return false
}
func (u *User) hasSettingsFromGroups() bool {
for _, g := range u.Groups {
if g.Type != sdk.GroupTypeMembership {
return true
}
}
return false
}
func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
if !u.hasSettingsFromGroups() {
return
}
if u.groupSettingsApplied {
return
}
replacer := u.getGroupPlacehodersReplacer()
for _, g := range u.Groups {
if g.Type == sdk.GroupTypePrimary {
if group, ok := groupsMapping[g.Name]; ok {
u.mergeWithPrimaryGroup(group, replacer)
} else {
providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name)
}
break
}
}
for _, g := range u.Groups {
if g.Type == sdk.GroupTypeSecondary {
if group, ok := groupsMapping[g.Name]; ok {
u.mergeAdditiveProperties(group, sdk.GroupTypeSecondary, replacer)
} else {
providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name)
}
}
}
u.removeDuplicatesAfterGroupMerge()
}
// LoadAndApplyGroupSettings update the user by loading and applying the group settings
func (u *User) LoadAndApplyGroupSettings() error {
if !u.hasSettingsFromGroups() {
return nil
}
if u.groupSettingsApplied {
return nil
}
names := make([]string, 0, len(u.Groups))
var primaryGroupName string
for _, g := range u.Groups {
if g.Type == sdk.GroupTypePrimary {
primaryGroupName = g.Name
}
if g.Type != sdk.GroupTypeMembership {
names = append(names, g.Name)
}
}
groups, err := provider.getGroupsWithNames(names)
if err != nil {
return fmt.Errorf("unable to get groups: %w", err)
}
replacer := u.getGroupPlacehodersReplacer()
// make sure to always merge with the primary group first
for idx, g := range groups {
if g.Name == primaryGroupName {
u.mergeWithPrimaryGroup(g, replacer)
lastIdx := len(groups) - 1
groups[idx] = groups[lastIdx]
groups = groups[:lastIdx]
break
}
}
for _, g := range groups {
u.mergeAdditiveProperties(g, sdk.GroupTypeSecondary, replacer)
}
u.removeDuplicatesAfterGroupMerge()
return nil
}
func (u *User) getGroupPlacehodersReplacer() *strings.Replacer {
return strings.NewReplacer("%username%", u.Username)
}
func (u *User) replacePlaceholder(value string, replacer *strings.Replacer) string {
if value == "" {
return value
}
return replacer.Replace(value)
}
func (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem, replacer *strings.Replacer) vfs.Filesystem {
switch fsConfig.Provider {
case sdk.S3FilesystemProvider:
fsConfig.S3Config.KeyPrefix = u.replacePlaceholder(fsConfig.S3Config.KeyPrefix, replacer)
case sdk.GCSFilesystemProvider:
fsConfig.GCSConfig.KeyPrefix = u.replacePlaceholder(fsConfig.GCSConfig.KeyPrefix, replacer)
case sdk.AzureBlobFilesystemProvider:
fsConfig.AzBlobConfig.KeyPrefix = u.replacePlaceholder(fsConfig.AzBlobConfig.KeyPrefix, replacer)
case sdk.SFTPFilesystemProvider:
fsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username, replacer)
fsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix, replacer)
case sdk.HTTPFilesystemProvider:
fsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username, replacer)
}
return fsConfig
}
func (u *User) mergeWithPrimaryGroup(group Group, replacer *strings.Replacer) {
if group.UserSettings.HomeDir != "" {
u.HomeDir = u.replacePlaceholder(group.UserSettings.HomeDir, replacer)
}
if group.UserSettings.FsConfig.Provider != 0 {
u.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig, replacer)
}
if u.MaxSessions == 0 {
u.MaxSessions = group.UserSettings.MaxSessions
}
if u.QuotaSize == 0 {
u.QuotaSize = group.UserSettings.QuotaSize
}
if u.QuotaFiles == 0 {
u.QuotaFiles = group.UserSettings.QuotaFiles
}
if u.UploadBandwidth == 0 {
u.UploadBandwidth = group.UserSettings.UploadBandwidth
}
if u.DownloadBandwidth == 0 {
u.DownloadBandwidth = group.UserSettings.DownloadBandwidth
}
if !u.hasMainDataTransferLimits() {
u.UploadDataTransfer = group.UserSettings.UploadDataTransfer
u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer
u.TotalDataTransfer = group.UserSettings.TotalDataTransfer
}
if u.ExpirationDate == 0 && group.UserSettings.ExpiresIn > 0 {
u.ExpirationDate = u.CreatedAt + int64(group.UserSettings.ExpiresIn)*86400000
}
u.mergePrimaryGroupFilters(group.UserSettings.Filters, replacer)
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer)
}
func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters, replacer *strings.Replacer) {
if u.Filters.MaxUploadFileSize == 0 {
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
}
if !u.IsTLSUsernameVerificationEnabled() {
u.Filters.TLSUsername = filters.TLSUsername
}
if !u.Filters.Hooks.CheckPasswordDisabled {
u.Filters.Hooks.CheckPasswordDisabled = filters.Hooks.CheckPasswordDisabled
}
if !u.Filters.Hooks.PreLoginDisabled {
u.Filters.Hooks.PreLoginDisabled = filters.Hooks.PreLoginDisabled
}
if !u.Filters.Hooks.ExternalAuthDisabled {
u.Filters.Hooks.ExternalAuthDisabled = filters.Hooks.ExternalAuthDisabled
}
if !u.Filters.DisableFsChecks {
u.Filters.DisableFsChecks = filters.DisableFsChecks
}
if !u.Filters.AllowAPIKeyAuth {
u.Filters.AllowAPIKeyAuth = filters.AllowAPIKeyAuth
}
if !u.Filters.IsAnonymous {
u.Filters.IsAnonymous = filters.IsAnonymous
}
if u.Filters.ExternalAuthCacheTime == 0 {
u.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime
}
if u.Filters.FTPSecurity == 0 {
u.Filters.FTPSecurity = filters.FTPSecurity
}
if u.Filters.StartDirectory == "" {
u.Filters.StartDirectory = u.replacePlaceholder(filters.StartDirectory, replacer)
}
if u.Filters.DefaultSharesExpiration == 0 {
u.Filters.DefaultSharesExpiration = filters.DefaultSharesExpiration
}
if u.Filters.PasswordExpiration == 0 {
u.Filters.PasswordExpiration = filters.PasswordExpiration
}
if u.Filters.PasswordStrength == 0 {
u.Filters.PasswordStrength = filters.PasswordStrength
}
}
func (u *User) mergeAdditiveProperties(group Group, groupType int, replacer *strings.Replacer) {
u.mergeVirtualFolders(group, groupType, replacer)
u.mergePermissions(group, groupType, replacer)
u.mergeFilePatterns(group, groupType, replacer)
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...)
u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)
u.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...)
u.Filters.DeniedProtocols = append(u.Filters.DeniedProtocols, group.UserSettings.Filters.DeniedProtocols...)
u.Filters.WebClient = append(u.Filters.WebClient, group.UserSettings.Filters.WebClient...)
u.Filters.TwoFactorAuthProtocols = append(u.Filters.TwoFactorAuthProtocols, group.UserSettings.Filters.TwoFactorAuthProtocols...)
}
func (u *User) mergeVirtualFolders(group Group, groupType int, replacer *strings.Replacer) {
if len(group.VirtualFolders) > 0 {
folderPaths := make(map[string]bool)
for _, folder := range u.VirtualFolders {
folderPaths[folder.VirtualPath] = true
}
for _, folder := range group.VirtualFolders {
if folder.VirtualPath == "/" && groupType != sdk.GroupTypePrimary {
continue
}
folder.VirtualPath = u.replacePlaceholder(folder.VirtualPath, replacer)
if _, ok := folderPaths[folder.VirtualPath]; !ok {
folder.MappedPath = u.replacePlaceholder(folder.MappedPath, replacer)
folder.FsConfig = u.replaceFsConfigPlaceholders(folder.FsConfig, replacer)
u.VirtualFolders = append(u.VirtualFolders, folder)
}
}
}
}
func (u *User) mergePermissions(group Group, groupType int, replacer *strings.Replacer) {
for k, v := range group.UserSettings.Permissions {
if k == "/" {
if groupType == sdk.GroupTypePrimary {
u.Permissions[k] = v
} else {
continue
}
}
k = u.replacePlaceholder(k, replacer)
if _, ok := u.Permissions[k]; !ok {
u.Permissions[k] = v
}
}
}
func (u *User) mergeFilePatterns(group Group, groupType int, replacer *strings.Replacer) {
if len(group.UserSettings.Filters.FilePatterns) > 0 {
patternPaths := make(map[string]bool)
for _, pattern := range u.Filters.FilePatterns {
patternPaths[pattern.Path] = true
}
for _, pattern := range group.UserSettings.Filters.FilePatterns {
if pattern.Path == "/" && groupType != sdk.GroupTypePrimary {
continue
}
pattern.Path = u.replacePlaceholder(pattern.Path, replacer)
if _, ok := patternPaths[pattern.Path]; !ok {
u.Filters.FilePatterns = append(u.Filters.FilePatterns, pattern)
}
}
}
}
func (u *User) removeDuplicatesAfterGroupMerge() {
u.Filters.AllowedIP = util.RemoveDuplicates(u.Filters.AllowedIP, false)
u.Filters.DeniedIP = util.RemoveDuplicates(u.Filters.DeniedIP, false)
u.Filters.DeniedLoginMethods = util.RemoveDuplicates(u.Filters.DeniedLoginMethods, false)
u.Filters.DeniedProtocols = util.RemoveDuplicates(u.Filters.DeniedProtocols, false)
u.Filters.WebClient = util.RemoveDuplicates(u.Filters.WebClient, false)
u.Filters.TwoFactorAuthProtocols = util.RemoveDuplicates(u.Filters.TwoFactorAuthProtocols, false)
u.SetEmptySecretsIfNil()
u.groupSettingsApplied = true
}
func (u *User) hasRole(role string) bool {
if role == "" {
return true
}
return role == u.Role
}
func (u *User) getACopy() User {
u.SetEmptySecretsIfNil()
pubKeys := make([]string, len(u.PublicKeys))
copy(pubKeys, u.PublicKeys)
virtualFolders := make([]vfs.VirtualFolder, 0, len(u.VirtualFolders))
for idx := range u.VirtualFolders {
vfolder := u.VirtualFolders[idx].GetACopy()
virtualFolders = append(virtualFolders, vfolder)
}
groups := make([]sdk.GroupMapping, 0, len(u.Groups))
for _, g := range u.Groups {
groups = append(groups, sdk.GroupMapping{
Name: g.Name,
Type: g.Type,
})
}
permissions := make(map[string][]string)
for k, v := range u.Permissions {
perms := make([]string, len(v))
copy(perms, v)
permissions[k] = perms
}
filters := UserFilters{
BaseUserFilters: copyBaseUserFilters(u.Filters.BaseUserFilters),
}
filters.RequirePasswordChange = u.Filters.RequirePasswordChange
filters.TOTPConfig.Enabled = u.Filters.TOTPConfig.Enabled
filters.TOTPConfig.ConfigName = u.Filters.TOTPConfig.ConfigName
filters.TOTPConfig.Secret = u.Filters.TOTPConfig.Secret.Clone()
filters.TOTPConfig.Protocols = make([]string, len(u.Filters.TOTPConfig.Protocols))
copy(filters.TOTPConfig.Protocols, u.Filters.TOTPConfig.Protocols)
filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))
for _, code := range u.Filters.RecoveryCodes {
if code.Secret == nil {
code.Secret = kms.NewEmptySecret()
}
filters.RecoveryCodes = append(filters.RecoveryCodes, RecoveryCode{
Secret: code.Secret.Clone(),
Used: code.Used,
})
}
return User{
BaseUser: sdk.BaseUser{
ID: u.ID,
Username: u.Username,
Email: u.Email,
Password: u.Password,
PublicKeys: pubKeys,
HasPassword: u.HasPassword,
HomeDir: u.HomeDir,
UID: u.UID,
GID: u.GID,
MaxSessions: u.MaxSessions,
QuotaSize: u.QuotaSize,
QuotaFiles: u.QuotaFiles,
Permissions: permissions,
UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles,
LastQuotaUpdate: u.LastQuotaUpdate,
UploadBandwidth: u.UploadBandwidth,
DownloadBandwidth: u.DownloadBandwidth,
UploadDataTransfer: u.UploadDataTransfer,
DownloadDataTransfer: u.DownloadDataTransfer,
TotalDataTransfer: u.TotalDataTransfer,
UsedUploadDataTransfer: u.UsedUploadDataTransfer,
UsedDownloadDataTransfer: u.UsedDownloadDataTransfer,
Status: u.Status,
ExpirationDate: u.ExpirationDate,
LastLogin: u.LastLogin,
FirstDownload: u.FirstDownload,
FirstUpload: u.FirstUpload,
LastPasswordChange: u.LastPasswordChange,
AdditionalInfo: u.AdditionalInfo,
Description: u.Description,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
Role: u.Role,
},
Filters: filters,
VirtualFolders: virtualFolders,
Groups: groups,
FsConfig: u.FsConfig.GetACopy(),
groupSettingsApplied: u.groupSettingsApplied,
}
}
// GetEncryptionAdditionalData returns the additional data to use for AEAD
func (u *User) GetEncryptionAdditionalData() string {
return u.Username
}