2019-07-30 18:51:29 +00:00
// Package dataprovider provides data access.
2020-06-07 21:30:18 +00:00
// It abstracts different data providers and exposes a common API.
2019-07-20 10:26:52 +00:00
package dataprovider
import (
2020-01-21 09:54:05 +00:00
"bufio"
2019-11-14 10:06:03 +00:00
"bytes"
2020-01-06 20:42:41 +00:00
"context"
2019-08-17 13:20:49 +00:00
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/base64"
2019-11-14 10:06:03 +00:00
"encoding/json"
2019-08-12 16:31:31 +00:00
"errors"
2019-07-20 10:26:52 +00:00
"fmt"
2019-08-17 13:20:49 +00:00
"hash"
2020-02-16 10:43:52 +00:00
"io"
2020-01-31 18:04:00 +00:00
"io/ioutil"
2019-12-30 17:37:50 +00:00
"net"
2019-11-14 10:06:03 +00:00
"net/http"
"net/url"
"os"
"os/exec"
2019-12-25 17:20:19 +00:00
"path"
2019-07-20 10:26:52 +00:00
"path/filepath"
2020-01-21 09:54:05 +00:00
"runtime"
2019-08-17 13:20:49 +00:00
"strconv"
2019-07-20 10:26:52 +00:00
"strings"
2020-01-21 09:54:05 +00:00
"sync"
2019-09-13 16:45:36 +00:00
"time"
2019-07-20 10:26:52 +00:00
2020-09-04 19:08:09 +00:00
"github.com/GehirnInc/crypt"
"github.com/GehirnInc/crypt/apr1_crypt"
"github.com/GehirnInc/crypt/md5_crypt"
"github.com/GehirnInc/crypt/sha512_crypt"
2019-07-20 10:26:52 +00:00
"github.com/alexedwards/argon2id"
2020-04-01 21:25:23 +00:00
"github.com/go-chi/render"
"github.com/rs/xid"
2019-08-12 16:31:31 +00:00
"golang.org/x/crypto/bcrypt"
2019-08-17 13:20:49 +00:00
"golang.org/x/crypto/pbkdf2"
2019-07-20 10:26:52 +00:00
"golang.org/x/crypto/ssh"
2020-04-26 21:29:09 +00:00
"github.com/drakkan/sftpgo/httpclient"
2019-08-12 16:31:31 +00:00
"github.com/drakkan/sftpgo/logger"
2019-09-13 16:45:36 +00:00
"github.com/drakkan/sftpgo/metrics"
2019-07-20 10:26:52 +00:00
"github.com/drakkan/sftpgo/utils"
2020-01-19 06:41:05 +00:00
"github.com/drakkan/sftpgo/vfs"
2019-07-20 10:26:52 +00:00
)
const (
2019-08-12 16:31:31 +00:00
// SQLiteDataProviderName name for SQLite database provider
2019-07-20 10:26:52 +00:00
SQLiteDataProviderName = "sqlite"
2019-09-06 09:23:06 +00:00
// PGSQLDataProviderName name for PostgreSQL database provider
PGSQLDataProviderName = "postgresql"
2019-08-12 16:31:31 +00:00
// MySQLDataProviderName name for MySQL database provider
2019-07-20 10:26:52 +00:00
MySQLDataProviderName = "mysql"
2019-08-12 16:31:31 +00:00
// BoltDataProviderName name for bbolt key/value store provider
BoltDataProviderName = "bolt"
2019-10-25 16:37:12 +00:00
// MemoryDataProviderName name for memory provider
MemoryDataProviderName = "memory"
2020-11-22 20:53:04 +00:00
// DumpVersion defines the version for the dump.
// For restore/load we support the current version and the previous one
DumpVersion = 5
2019-07-20 10:26:52 +00:00
2020-04-11 10:25:21 +00:00
argonPwdPrefix = "$argon2id$"
bcryptPwdPrefix = "$2a$"
pbkdf2SHA1Prefix = "$pbkdf2-sha1$"
pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
pbkdf2SHA256B64SaltPrefix = "$pbkdf2-b64salt-sha256$"
md5cryptPwdPrefix = "$1$"
md5cryptApr1PwdPrefix = "$apr1$"
sha512cryptPwdPrefix = "$6$"
manageUsersDisabledError = "please set manage_users to 1 in your configuration to enable this method"
trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
operationAdd = "add"
operationUpdate = "update"
operationDelete = "delete"
2020-06-07 21:30:18 +00:00
sqlPrefixValidChars = "abcdefghijklmnopqrstuvwxyz_"
)
// ordering constants
const (
OrderASC = "ASC"
OrderDESC = "DESC"
2019-07-20 10:26:52 +00:00
)
var (
2020-04-09 21:32:42 +00:00
// SupportedProviders defines the supported data providers
2019-10-25 16:37:12 +00:00
SupportedProviders = [ ] string { SQLiteDataProviderName , PGSQLDataProviderName , MySQLDataProviderName ,
BoltDataProviderName , MemoryDataProviderName }
2020-04-09 21:32:42 +00:00
// ValidPerms defines all the valid permissions for a user
2019-10-07 16:19:01 +00:00
ValidPerms = [ ] string { PermAny , PermListItems , PermDownload , PermUpload , PermOverwrite , PermRename , PermDelete ,
2019-11-16 09:23:41 +00:00
PermCreateDirs , PermCreateSymlinks , PermChmod , PermChown , PermChtimes }
2020-04-09 21:32:42 +00:00
// ValidSSHLoginMethods defines all the valid SSH login methods
2020-08-12 14:15:12 +00:00
ValidSSHLoginMethods = [ ] string { SSHLoginMethodPublicKey , LoginMethodPassword , SSHLoginMethodKeyboardInteractive ,
2020-04-09 21:32:42 +00:00
SSHLoginMethodKeyAndPassword , SSHLoginMethodKeyAndKeyboardInt }
// SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications
SSHMultiStepsLoginMethods = [ ] string { SSHLoginMethodKeyAndPassword , SSHLoginMethodKeyAndKeyboardInt }
2020-08-12 14:15:12 +00:00
// ErrNoAuthTryed defines the error for connection closed before authentication
2020-08-17 10:49:20 +00:00
ErrNoAuthTryed = errors . New ( "no auth tryed" )
// ValidProtocols defines all the valid protcols
2020-08-30 11:50:43 +00:00
ValidProtocols = [ ] string { "SSH" , "FTP" , "DAV" }
2020-10-05 17:42:33 +00:00
// ErrNoInitRequired defines the error returned by InitProvider if no inizialization/update is required
ErrNoInitRequired = errors . New ( "The data provider is already up to date" )
2020-08-31 17:25:17 +00:00
// ErrInvalidCredentials defines the error to return if the supplied credentials are invalid
ErrInvalidCredentials = errors . New ( "Invalid credentials" )
webDAVUsersCache sync . Map
config Config
provider Provider
sqlPlaceholders [ ] string
hashPwdPrefixes = [ ] string { argonPwdPrefix , bcryptPwdPrefix , pbkdf2SHA1Prefix , pbkdf2SHA256Prefix ,
2020-04-11 10:25:21 +00:00
pbkdf2SHA512Prefix , pbkdf2SHA256B64SaltPrefix , md5cryptPwdPrefix , md5cryptApr1PwdPrefix , sha512cryptPwdPrefix }
pbkdfPwdPrefixes = [ ] string { pbkdf2SHA1Prefix , pbkdf2SHA256Prefix , pbkdf2SHA512Prefix , pbkdf2SHA256B64SaltPrefix }
pbkdfPwdB64SaltPrefixes = [ ] string { pbkdf2SHA256B64SaltPrefix }
unixPwdPrefixes = [ ] string { md5cryptPwdPrefix , md5cryptApr1PwdPrefix , sha512cryptPwdPrefix }
logSender = "dataProvider"
availabilityTicker * time . Ticker
availabilityTickerDone chan bool
credentialsDirPath string
2020-06-07 21:30:18 +00:00
sqlTableUsers = "users"
sqlTableFolders = "folders"
sqlTableFoldersMapping = "folders_mapping"
sqlTableSchemaVersion = "schema_version"
2020-09-04 15:09:31 +00:00
argon2Params * argon2id . Params
2019-07-20 10:26:52 +00:00
)
2020-02-08 13:44:25 +00:00
type schemaVersion struct {
Version int
}
2020-09-04 15:09:31 +00:00
// Argon2Options defines the options for argon2 password hashing
type Argon2Options struct {
Memory uint32 ` json:"memory" mapstructure:"memory" `
Iterations uint32 ` json:"iterations" mapstructure:"iterations" `
Parallelism uint8 ` json:"parallelism" mapstructure:"parallelism" `
}
// PasswordHashing defines the configuration for password hashing
type PasswordHashing struct {
Argon2Options Argon2Options ` json:"argon2_options" mapstructure:"argon2_options" `
}
2020-07-24 21:39:38 +00:00
// UserActions defines the action to execute on user create, update, delete.
type UserActions struct {
2019-11-14 10:06:03 +00:00
// Valid values are add, update, delete. Empty slice to disable
ExecuteOn [ ] string ` json:"execute_on" mapstructure:"execute_on" `
2020-05-24 13:29:39 +00:00
// Absolute path to an external program or an HTTP URL
Hook string ` json:"hook" mapstructure:"hook" `
2019-11-14 10:06:03 +00:00
}
2019-07-20 10:26:52 +00:00
// Config provider configuration
type Config struct {
2019-07-30 18:51:29 +00:00
// Driver name, must be one of the SupportedProviders
2019-08-07 20:46:13 +00:00
Driver string ` json:"driver" mapstructure:"driver" `
2020-01-06 20:42:41 +00:00
// Database name. For driver sqlite this can be the database name relative to the config dir
// or the absolute path to the SQLite database.
2019-08-07 20:46:13 +00:00
Name string ` json:"name" mapstructure:"name" `
2019-07-30 18:51:29 +00:00
// Database host
2019-08-07 20:46:13 +00:00
Host string ` json:"host" mapstructure:"host" `
2019-07-30 18:51:29 +00:00
// Database port
2019-08-07 20:46:13 +00:00
Port int ` json:"port" mapstructure:"port" `
2019-07-30 18:51:29 +00:00
// Database username
2019-08-07 20:46:13 +00:00
Username string ` json:"username" mapstructure:"username" `
2019-07-30 18:51:29 +00:00
// Database password
2019-08-07 20:46:13 +00:00
Password string ` json:"password" mapstructure:"password" `
2019-07-30 18:51:29 +00:00
// Used for drivers mysql and postgresql.
// 0 disable SSL/TLS connections.
// 1 require ssl.
// 2 set ssl mode to verify-ca for driver postgresql and skip-verify for driver mysql.
// 3 set ssl mode to verify-full for driver postgresql and preferred for driver mysql.
2019-08-07 20:46:13 +00:00
SSLMode int ` json:"sslmode" mapstructure:"sslmode" `
2019-07-30 18:51:29 +00:00
// Custom database connection string.
// If not empty this connection string will be used instead of build one using the previous parameters
2019-08-07 20:46:13 +00:00
ConnectionString string ` json:"connection_string" mapstructure:"connection_string" `
2020-06-07 21:30:18 +00:00
// prefix for SQL tables
SQLTablesPrefix string ` json:"sql_tables_prefix" mapstructure:"sql_tables_prefix" `
2019-07-30 18:51:29 +00:00
// Set to 0 to disable users management, 1 to enable
2019-08-07 20:46:13 +00:00
ManageUsers int ` json:"manage_users" mapstructure:"manage_users" `
2019-07-30 18:51:29 +00:00
// Set the preferred way to track users quota between the following choices:
// 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
// 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
2020-06-07 21:30:18 +00:00
// 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions
// and for virtual folders.
2019-07-30 18:51:29 +00:00
// With this configuration the "quota scan" REST API can still be used to periodically update space usage
// for users without quota restrictions
2019-08-07 20:46:13 +00:00
TrackQuota int ` json:"track_quota" mapstructure:"track_quota" `
2019-09-13 06:14:07 +00:00
// Sets the maximum number of open connections for mysql and postgresql driver.
// Default 0 (unlimited)
PoolSize int ` json:"pool_size" mapstructure:"pool_size" `
2020-03-03 22:25:23 +00:00
// Users default base directory.
2019-09-28 20:48:52 +00:00
// If no home dir is defined while adding a new user, and this value is
// a valid absolute path, then the user home dir will be automatically
// defined as the path obtained joining the base dir and the username
UsersBaseDir string ` json:"users_base_dir" mapstructure:"users_base_dir" `
2019-11-14 10:06:03 +00:00
// Actions to execute on user add, update, delete.
2019-11-14 16:43:14 +00:00
// Update action will not be fired for internal updates such as the last login or the user quota fields.
2020-07-24 21:39:38 +00:00
Actions UserActions ` json:"actions" mapstructure:"actions" `
2020-04-01 21:25:23 +00:00
// Absolute path to an external program or an HTTP URL to invoke for users authentication.
// Leave empty to use builtin authentication.
2020-01-06 20:42:41 +00:00
// If the authentication succeed the user will be automatically added/updated inside the defined data provider.
// Actions defined for user added/updated will not be executed in this case.
// This method is slower than built-in authentication methods, but it's very flexible as anyone can
2020-04-01 21:25:23 +00:00
// easily write his own authentication hooks.
ExternalAuthHook string ` json:"external_auth_hook" mapstructure:"external_auth_hook" `
// ExternalAuthScope defines the scope for the external authentication hook.
2020-10-05 09:29:18 +00:00
// - 0 means all supported authentication scopes, the external hook will be executed for password,
2020-01-21 09:54:05 +00:00
// public key and keyboard interactive authentication
// - 1 means passwords only
// - 2 means public keys only
// - 4 means keyboard interactive only
// you can combine the scopes, for example 3 means password and public key, 5 password and keyboard
// interactive and so on
2020-01-06 20:42:41 +00:00
ExternalAuthScope int ` json:"external_auth_scope" mapstructure:"external_auth_scope" `
2020-01-31 18:04:00 +00:00
// CredentialsPath defines the directory for storing user provided credential files such as
// Google Cloud Storage credentials. It can be a path relative to the config dir or an
// absolute path
CredentialsPath string ` json:"credentials_path" mapstructure:"credentials_path" `
2020-04-01 21:25:23 +00:00
// Absolute path to an external program or an HTTP URL to invoke just before the user login.
// This program/URL allows to modify or create the user trying to login.
// It is useful if you have users with dynamic fields to update just before the login.
// Please note that if you want to create a new user, the pre-login hook response must
2020-03-27 22:26:22 +00:00
// include all the mandatory user fields.
//
2020-04-01 21:25:23 +00:00
// The pre-login hook must finish within 30 seconds.
2020-02-23 17:50:59 +00:00
//
2020-04-01 21:25:23 +00:00
// If an error happens while executing the "PreLoginHook" then login will be denied.
// PreLoginHook and ExternalAuthHook are mutally exclusive.
2020-02-23 17:50:59 +00:00
// Leave empty to disable.
2020-04-01 21:25:23 +00:00
PreLoginHook string ` json:"pre_login_hook" mapstructure:"pre_login_hook" `
2020-08-12 14:15:12 +00:00
// Absolute path to an external program or an HTTP URL to invoke after the user login.
// Based on the configured scope you can choose if notify failed or successful logins
// or both
PostLoginHook string ` json:"post_login_hook" mapstructure:"post_login_hook" `
// PostLoginScope defines the scope for the post-login hook.
// - 0 means notify both failed and successful logins
// - 1 means notify failed logins
// - 2 means notify successful logins
PostLoginScope int ` json:"post_login_scope" mapstructure:"post_login_scope" `
2020-08-19 17:36:12 +00:00
// Absolute path to an external program or an HTTP URL to invoke just before password
// authentication. This hook allows you to externally check the provided password,
// its main use case is to allow to easily support things like password+OTP for protocols
// without keyboard interactive support such as FTP and WebDAV. You can ask your users
// to login using a string consisting of a fixed password and a One Time Token, you
// can verify the token inside the hook and ask to SFTPGo to verify the fixed part.
CheckPasswordHook string ` json:"check_password_hook" mapstructure:"check_password_hook" `
// CheckPasswordScope defines the scope for the check password hook.
// - 0 means all protocols
// - 1 means SSH
// - 2 means FTP
// - 4 means WebDAV
// you can combine the scopes, for example 6 means FTP and WebDAV
CheckPasswordScope int ` json:"check_password_scope" mapstructure:"check_password_scope" `
2020-10-05 17:42:33 +00:00
// Defines how the database will be initialized/updated:
// - 0 means automatically
// - 1 means manually using the initprovider sub-command
UpdateMode int ` json:"update_mode" mapstructure:"update_mode" `
2020-10-22 08:42:40 +00:00
// PasswordHashing defines the configuration for password hashing
PasswordHashing PasswordHashing ` json:"password_hashing" mapstructure:"password_hashing" `
// PreferDatabaseCredentials indicates whether credential files (currently used for Google
// Cloud Storage) should be stored in the database instead of in the directory specified by
// CredentialsPath.
PreferDatabaseCredentials bool ` json:"prefer_database_credentials" mapstructure:"prefer_database_credentials" `
2019-07-20 10:26:52 +00:00
}
2020-02-02 21:20:39 +00:00
// BackupData defines the structure for the backup/restore files
type BackupData struct {
2020-06-07 21:30:18 +00:00
Users [ ] User ` json:"users" `
Folders [ ] vfs . BaseVirtualFolder ` json:"folders" `
2020-11-22 20:53:04 +00:00
Version int ` json:"version" `
2020-02-02 21:20:39 +00:00
}
2020-04-01 21:25:23 +00:00
type keyboardAuthHookRequest struct {
RequestID string ` json:"request_id" `
Username string ` json:"username,omitempty" `
2020-08-04 16:03:28 +00:00
IP string ` json:"ip,omitempty" `
2020-04-01 21:25:23 +00:00
Password string ` json:"password,omitempty" `
Answers [ ] string ` json:"answers,omitempty" `
Questions [ ] string ` json:"questions,omitempty" `
}
type keyboardAuthHookResponse struct {
2020-01-21 09:54:05 +00:00
Instruction string ` json:"instruction" `
Questions [ ] string ` json:"questions" `
Echos [ ] bool ` json:"echos" `
AuthResult int ` json:"auth_result" `
2020-02-16 10:43:52 +00:00
CheckPwd int ` json:"check_password" `
2020-01-21 09:54:05 +00:00
}
2020-08-19 17:36:12 +00:00
type checkPasswordRequest struct {
Username string ` json:"username" `
IP string ` json:"ip" `
Password string ` json:"password" `
Protocol string ` json:"protocol" `
}
type checkPasswordResponse struct {
// 0 KO, 1 OK, 2 partial success, -1 not executed
Status int ` json:"status" `
// for status = 2 this is the password to check against the one stored
// inside the SFTPGo data provider
ToVerify string ` json:"to_verify" `
}
2020-06-07 21:30:18 +00:00
type virtualFoldersCompact struct {
VirtualPath string ` json:"virtual_path" `
MappedPath string ` json:"mapped_path" `
ExcludeFromQuota bool ` json:"exclude_from_quota" `
}
type userCompactVFolders struct {
ID int64 ` json:"id" `
Username string ` json:"username" `
VirtualFolders [ ] virtualFoldersCompact ` json:"virtual_folders" `
}
2019-07-20 10:26:52 +00:00
// ValidationError raised if input data is not valid
type ValidationError struct {
err string
}
2019-07-30 18:51:29 +00:00
// Validation error details
2019-07-20 10:26:52 +00:00
func ( e * ValidationError ) Error ( ) string {
return fmt . Sprintf ( "Validation error: %s" , e . err )
}
2019-07-30 18:51:29 +00:00
// MethodDisabledError raised if a method is disabled in config file.
// For example, if user management is disabled, this error is raised
2020-03-27 22:26:22 +00:00
// every time a user operation is done using the REST API
2019-07-20 10:26:52 +00:00
type MethodDisabledError struct {
err string
}
2019-07-30 18:51:29 +00:00
// Method disabled error details
2019-07-20 10:26:52 +00:00
func ( e * MethodDisabledError ) Error ( ) string {
return fmt . Sprintf ( "Method disabled error: %s" , e . err )
}
2019-08-12 16:31:31 +00:00
// RecordNotFoundError raised if a requested user is not found
type RecordNotFoundError struct {
err string
}
func ( e * RecordNotFoundError ) Error ( ) string {
return fmt . Sprintf ( "Not found: %s" , e . err )
}
2019-11-26 21:26:42 +00:00
// GetQuotaTracking returns the configured mode for user's quota tracking
func GetQuotaTracking ( ) int {
return config . TrackQuota
}
2020-06-07 21:30:18 +00:00
// Provider defines the interface that data providers must implement.
2019-07-20 10:26:52 +00:00
type Provider interface {
2020-08-19 17:36:12 +00:00
validateUserAndPass ( username , password , ip , protocol string ) ( User , error )
2020-04-09 21:32:42 +00:00
validateUserAndPubKey ( username string , pubKey [ ] byte ) ( User , string , error )
2019-07-20 10:26:52 +00:00
updateQuota ( username string , filesAdd int , sizeAdd int64 , reset bool ) error
getUsedQuota ( username string ) ( int , int64 , error )
userExists ( username string ) ( User , error )
addUser ( user User ) error
updateUser ( user User ) error
deleteUser ( user User ) error
getUsers ( limit int , offset int , order string , username string ) ( [ ] User , error )
2019-12-27 22:12:44 +00:00
dumpUsers ( ) ( [ ] User , error )
2019-07-20 10:26:52 +00:00
getUserByID ( ID int64 ) ( User , error )
2019-11-13 10:36:21 +00:00
updateLastLogin ( username string ) error
2020-06-07 21:30:18 +00:00
getFolders ( limit , offset int , order , folderPath string ) ( [ ] vfs . BaseVirtualFolder , error )
getFolderByPath ( mappedPath string ) ( vfs . BaseVirtualFolder , error )
addFolder ( folder vfs . BaseVirtualFolder ) error
deleteFolder ( folder vfs . BaseVirtualFolder ) error
updateFolderQuota ( mappedPath string , filesAdd int , sizeAdd int64 , reset bool ) error
getUsedFolderQuota ( mappedPath string ) ( int , int64 , error )
dumpFolders ( ) ( [ ] vfs . BaseVirtualFolder , error )
2019-09-13 16:45:36 +00:00
checkAvailability ( ) error
2019-09-28 20:48:52 +00:00
close ( ) error
2020-02-02 21:20:39 +00:00
reloadConfig ( ) error
2020-02-08 13:44:25 +00:00
initializeDatabase ( ) error
migrateDatabase ( ) error
2019-09-13 16:45:36 +00:00
}
2019-07-30 18:51:29 +00:00
// Initialize the data provider.
// An error is returned if the configured driver is invalid or if the data provider cannot be initialized
2019-07-20 10:26:52 +00:00
func Initialize ( cnf Config , basePath string ) error {
2019-09-13 16:45:36 +00:00
var err error
2019-07-20 10:26:52 +00:00
config = cnf
2020-01-06 20:42:41 +00:00
2020-04-01 21:25:23 +00:00
if err = validateHooks ( ) ; err != nil {
return err
2020-02-23 17:50:59 +00:00
}
2020-11-22 20:53:04 +00:00
if err = validateCredentialsDir ( basePath , cnf . PreferDatabaseCredentials ) ; err != nil {
return err
2020-02-08 13:44:25 +00:00
}
err = createProvider ( basePath )
if err != nil {
return err
}
2020-10-05 17:42:33 +00:00
if cnf . UpdateMode == 0 {
err = provider . initializeDatabase ( )
if err != nil && err != ErrNoInitRequired {
logger . WarnToConsole ( "Unable to initialize data provider: %v" , err )
providerLog ( logger . LevelWarn , "Unable to initialize data provider: %v" , err )
return err
}
if err == nil {
logger . DebugToConsole ( "Data provider successfully initialized" )
}
err = provider . migrateDatabase ( )
if err != nil && err != ErrNoInitRequired {
providerLog ( logger . LevelWarn , "database migration error: %v" , err )
return err
}
} else {
providerLog ( logger . LevelInfo , "database initialization/migration skipped, manual mode is configured" )
2020-01-31 18:04:00 +00:00
}
2020-09-04 15:09:31 +00:00
argon2Params = & argon2id . Params {
Memory : cnf . PasswordHashing . Argon2Options . Memory ,
Iterations : cnf . PasswordHashing . Argon2Options . Iterations ,
Parallelism : cnf . PasswordHashing . Argon2Options . Parallelism ,
SaltLength : 16 ,
KeyLength : 32 ,
}
2020-02-08 13:44:25 +00:00
startAvailabilityTimer ( )
return nil
}
2020-01-06 20:42:41 +00:00
2020-04-01 21:25:23 +00:00
func validateHooks ( ) error {
2020-08-19 17:36:12 +00:00
var hooks [ ] string
2020-04-01 21:25:23 +00:00
if len ( config . PreLoginHook ) > 0 && ! strings . HasPrefix ( config . PreLoginHook , "http" ) {
2020-08-19 17:36:12 +00:00
hooks = append ( hooks , config . PreLoginHook )
2020-04-01 21:25:23 +00:00
}
if len ( config . ExternalAuthHook ) > 0 && ! strings . HasPrefix ( config . ExternalAuthHook , "http" ) {
2020-08-19 17:36:12 +00:00
hooks = append ( hooks , config . ExternalAuthHook )
2020-04-01 21:25:23 +00:00
}
2020-08-12 14:15:12 +00:00
if len ( config . PostLoginHook ) > 0 && ! strings . HasPrefix ( config . PostLoginHook , "http" ) {
2020-08-19 17:36:12 +00:00
hooks = append ( hooks , config . PostLoginHook )
}
if len ( config . CheckPasswordHook ) > 0 && ! strings . HasPrefix ( config . CheckPasswordHook , "http" ) {
hooks = append ( hooks , config . CheckPasswordHook )
}
for _ , hook := range hooks {
if ! filepath . IsAbs ( hook ) {
return fmt . Errorf ( "invalid hook: %#v must be an absolute path" , hook )
2020-08-12 14:15:12 +00:00
}
2020-08-19 17:36:12 +00:00
_ , err := os . Stat ( hook )
2020-08-12 14:15:12 +00:00
if err != nil {
2020-08-19 17:36:12 +00:00
providerLog ( logger . LevelWarn , "invalid hook: %v" , err )
2020-08-12 14:15:12 +00:00
return err
}
}
2020-08-19 17:36:12 +00:00
2020-04-01 21:25:23 +00:00
return nil
}
2020-06-07 21:30:18 +00:00
func validateSQLTablesPrefix ( ) error {
if len ( config . SQLTablesPrefix ) > 0 {
for _ , char := range config . SQLTablesPrefix {
if ! strings . Contains ( sqlPrefixValidChars , strings . ToLower ( string ( char ) ) ) {
return errors . New ( "Invalid sql_tables_prefix only chars in range 'a..z', 'A..Z' and '_' are allowed" )
}
}
sqlTableUsers = config . SQLTablesPrefix + sqlTableUsers
sqlTableFolders = config . SQLTablesPrefix + sqlTableFolders
sqlTableFoldersMapping = config . SQLTablesPrefix + sqlTableFoldersMapping
sqlTableSchemaVersion = config . SQLTablesPrefix + sqlTableSchemaVersion
providerLog ( logger . LevelDebug , "sql table for users %#v, folders %#v folders mapping %#v schema version %#v" ,
sqlTableUsers , sqlTableFolders , sqlTableFoldersMapping , sqlTableSchemaVersion )
}
return nil
}
2020-02-08 13:44:25 +00:00
// InitializeDatabase creates the initial database structure
func InitializeDatabase ( cnf Config , basePath string ) error {
config = cnf
err := createProvider ( basePath )
if err != nil {
return err
2019-07-20 10:26:52 +00:00
}
2020-10-05 17:42:33 +00:00
err = provider . initializeDatabase ( )
if err != nil && err != ErrNoInitRequired {
return err
}
return provider . migrateDatabase ( )
2019-07-20 10:26:52 +00:00
}
2019-07-30 18:51:29 +00:00
// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
2020-08-12 14:15:12 +00:00
func CheckUserAndPass ( username , password , ip , protocol string ) ( User , error ) {
2020-04-01 21:25:23 +00:00
if len ( config . ExternalAuthHook ) > 0 && ( config . ExternalAuthScope == 0 || config . ExternalAuthScope & 1 != 0 ) {
2020-08-12 14:15:12 +00:00
user , err := doExternalAuth ( username , password , nil , "" , ip , protocol )
2020-01-06 20:42:41 +00:00
if err != nil {
return user , err
}
2020-08-19 17:36:12 +00:00
return checkUserAndPass ( user , password , ip , protocol )
2020-01-06 20:42:41 +00:00
}
2020-04-01 21:25:23 +00:00
if len ( config . PreLoginHook ) > 0 {
2020-08-12 14:15:12 +00:00
user , err := executePreLoginHook ( username , LoginMethodPassword , ip , protocol )
2020-02-23 17:50:59 +00:00
if err != nil {
return user , err
}
2020-08-19 17:36:12 +00:00
return checkUserAndPass ( user , password , ip , protocol )
2020-02-23 17:50:59 +00:00
}
2020-08-19 17:36:12 +00:00
return provider . validateUserAndPass ( username , password , ip , protocol )
2019-07-20 10:26:52 +00:00
}
2019-07-30 18:51:29 +00:00
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
2020-08-12 14:15:12 +00:00
func CheckUserAndPubKey ( username string , pubKey [ ] byte , ip , protocol string ) ( User , string , error ) {
2020-04-01 21:25:23 +00:00
if len ( config . ExternalAuthHook ) > 0 && ( config . ExternalAuthScope == 0 || config . ExternalAuthScope & 2 != 0 ) {
2020-08-12 14:15:12 +00:00
user , err := doExternalAuth ( username , "" , pubKey , "" , ip , protocol )
2020-01-06 20:42:41 +00:00
if err != nil {
return user , "" , err
}
return checkUserAndPubKey ( user , pubKey )
}
2020-04-01 21:25:23 +00:00
if len ( config . PreLoginHook ) > 0 {
2020-08-12 14:15:12 +00:00
user , err := executePreLoginHook ( username , SSHLoginMethodPublicKey , ip , protocol )
2020-02-23 17:50:59 +00:00
if err != nil {
return user , "" , err
}
return checkUserAndPubKey ( user , pubKey )
}
2020-07-08 17:59:31 +00:00
return provider . validateUserAndPubKey ( username , pubKey )
2019-07-20 10:26:52 +00:00
}
2020-01-21 09:54:05 +00:00
// CheckKeyboardInteractiveAuth checks the keyboard interactive authentication and returns
// the authenticated user or an error
2020-08-12 14:15:12 +00:00
func CheckKeyboardInteractiveAuth ( username , authHook string , client ssh . KeyboardInteractiveChallenge , ip , protocol string ) ( User , error ) {
2020-01-21 09:54:05 +00:00
var user User
var err error
2020-04-01 21:25:23 +00:00
if len ( config . ExternalAuthHook ) > 0 && ( config . ExternalAuthScope == 0 || config . ExternalAuthScope & 4 != 0 ) {
2020-08-12 14:15:12 +00:00
user , err = doExternalAuth ( username , "" , nil , "1" , ip , protocol )
2020-04-01 21:25:23 +00:00
} else if len ( config . PreLoginHook ) > 0 {
2020-08-12 14:15:12 +00:00
user , err = executePreLoginHook ( username , SSHLoginMethodKeyboardInteractive , ip , protocol )
2020-01-21 09:54:05 +00:00
} else {
2020-07-08 17:59:31 +00:00
user , err = provider . userExists ( username )
2020-01-21 09:54:05 +00:00
}
if err != nil {
return user , err
}
2020-08-19 17:36:12 +00:00
return doKeyboardInteractiveAuth ( user , authHook , client , ip , protocol )
2020-01-21 09:54:05 +00:00
}
2019-11-13 10:36:21 +00:00
// UpdateLastLogin updates the last login fields for the given SFTP user
2020-07-08 17:59:31 +00:00
func UpdateLastLogin ( user User ) error {
2019-11-13 10:36:21 +00:00
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-07-08 17:59:31 +00:00
return provider . updateLastLogin ( user . Username )
2019-11-13 10:36:21 +00:00
}
2019-07-30 18:51:29 +00:00
// UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd.
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
2020-07-08 17:59:31 +00:00
func UpdateUserQuota ( user User , filesAdd int , sizeAdd int64 , reset bool ) error {
2019-07-20 10:26:52 +00:00
if config . TrackQuota == 0 {
return & MethodDisabledError { err : trackQuotaDisabledError }
2019-07-28 20:04:50 +00:00
} else if config . TrackQuota == 2 && ! reset && ! user . HasQuotaRestrictions ( ) {
return nil
2019-07-20 10:26:52 +00:00
}
2019-11-13 10:36:21 +00:00
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-08-31 17:25:17 +00:00
if filesAdd == 0 && sizeAdd == 0 && ! reset {
return nil
}
2020-07-08 17:59:31 +00:00
return provider . updateQuota ( user . Username , filesAdd , sizeAdd , reset )
2019-07-20 10:26:52 +00:00
}
2020-06-07 21:30:18 +00:00
// UpdateVirtualFolderQuota updates the quota for the given virtual folder adding filesAdd and sizeAdd.
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
2020-07-08 17:59:31 +00:00
func UpdateVirtualFolderQuota ( vfolder vfs . BaseVirtualFolder , filesAdd int , sizeAdd int64 , reset bool ) error {
2020-06-07 21:30:18 +00:00
if config . TrackQuota == 0 {
return & MethodDisabledError { err : trackQuotaDisabledError }
}
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-08-31 17:25:17 +00:00
if filesAdd == 0 && sizeAdd == 0 && ! reset {
return nil
}
2020-07-08 17:59:31 +00:00
return provider . updateFolderQuota ( vfolder . MappedPath , filesAdd , sizeAdd , reset )
2020-06-07 21:30:18 +00:00
}
2019-07-30 18:51:29 +00:00
// GetUsedQuota returns the used quota for the given SFTP user.
2020-07-08 17:59:31 +00:00
func GetUsedQuota ( username string ) ( int , int64 , error ) {
2019-07-20 10:26:52 +00:00
if config . TrackQuota == 0 {
return 0 , 0 , & MethodDisabledError { err : trackQuotaDisabledError }
}
2020-07-08 17:59:31 +00:00
return provider . getUsedQuota ( username )
2019-07-20 10:26:52 +00:00
}
2020-06-07 21:30:18 +00:00
// GetUsedVirtualFolderQuota returns the used quota for the given virtual folder.
2020-07-08 17:59:31 +00:00
func GetUsedVirtualFolderQuota ( mappedPath string ) ( int , int64 , error ) {
2020-06-07 21:30:18 +00:00
if config . TrackQuota == 0 {
return 0 , 0 , & MethodDisabledError { err : trackQuotaDisabledError }
}
2020-07-08 17:59:31 +00:00
return provider . getUsedFolderQuota ( mappedPath )
2020-06-07 21:30:18 +00:00
}
2019-07-30 18:51:29 +00:00
// UserExists checks if the given SFTP username exists, returns an error if no match is found
2020-07-08 17:59:31 +00:00
func UserExists ( username string ) ( User , error ) {
return provider . userExists ( username )
2019-07-20 10:26:52 +00:00
}
2020-08-31 17:25:17 +00:00
// AddUser adds a new SFTPGo user.
2019-07-30 18:51:29 +00:00
// ManageUsers configuration must be set to 1 to enable this method
2020-07-08 17:59:31 +00:00
func AddUser ( user User ) error {
2019-07-20 10:26:52 +00:00
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-07-08 17:59:31 +00:00
err := provider . addUser ( user )
2019-11-14 10:06:03 +00:00
if err == nil {
2020-05-24 21:31:14 +00:00
go executeAction ( operationAdd , user )
2019-11-14 10:06:03 +00:00
}
return err
2019-07-20 10:26:52 +00:00
}
2020-08-31 17:25:17 +00:00
// UpdateUser updates an existing SFTPGo user.
2019-07-30 18:51:29 +00:00
// ManageUsers configuration must be set to 1 to enable this method
2020-07-08 17:59:31 +00:00
func UpdateUser ( user User ) error {
2019-07-20 10:26:52 +00:00
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-07-08 17:59:31 +00:00
err := provider . updateUser ( user )
2019-11-14 10:06:03 +00:00
if err == nil {
2020-08-31 17:25:17 +00:00
RemoveCachedWebDAVUser ( user . Username )
2020-05-24 21:31:14 +00:00
go executeAction ( operationUpdate , user )
2019-11-14 10:06:03 +00:00
}
return err
2019-07-20 10:26:52 +00:00
}
2020-09-01 14:10:26 +00:00
// DeleteUser deletes an existing SFTPGo user.
2019-07-30 18:51:29 +00:00
// ManageUsers configuration must be set to 1 to enable this method
2020-07-08 17:59:31 +00:00
func DeleteUser ( user User ) error {
2019-07-20 10:26:52 +00:00
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-07-08 17:59:31 +00:00
err := provider . deleteUser ( user )
2019-11-14 10:06:03 +00:00
if err == nil {
2020-08-31 17:25:17 +00:00
RemoveCachedWebDAVUser ( user . Username )
2020-05-24 21:31:14 +00:00
go executeAction ( operationDelete , user )
2019-11-14 10:06:03 +00:00
}
return err
2019-07-20 10:26:52 +00:00
}
2020-02-02 21:20:39 +00:00
// ReloadConfig reloads provider configuration.
// Currently only implemented for memory provider, allows to reload the users
// from the configured file, if defined
func ReloadConfig ( ) error {
return provider . reloadConfig ( )
}
2019-07-30 18:51:29 +00:00
// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
2020-07-08 17:59:31 +00:00
func GetUsers ( limit , offset int , order string , username string ) ( [ ] User , error ) {
return provider . getUsers ( limit , offset , order , username )
2019-07-20 10:26:52 +00:00
}
2019-07-30 18:51:29 +00:00
// GetUserByID returns the user with the given database ID if a match is found or an error
2020-07-08 17:59:31 +00:00
func GetUserByID ( ID int64 ) ( User , error ) {
return provider . getUserByID ( ID )
2019-07-20 10:26:52 +00:00
}
2020-06-07 21:30:18 +00:00
// AddFolder adds a new virtual folder.
// ManageUsers configuration must be set to 1 to enable this method
2020-07-08 17:59:31 +00:00
func AddFolder ( folder vfs . BaseVirtualFolder ) error {
2020-06-07 21:30:18 +00:00
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-07-08 17:59:31 +00:00
return provider . addFolder ( folder )
2020-06-07 21:30:18 +00:00
}
// DeleteFolder deletes an existing folder.
// ManageUsers configuration must be set to 1 to enable this method
2020-07-08 17:59:31 +00:00
func DeleteFolder ( folder vfs . BaseVirtualFolder ) error {
2020-06-07 21:30:18 +00:00
if config . ManageUsers == 0 {
return & MethodDisabledError { err : manageUsersDisabledError }
}
2020-07-08 17:59:31 +00:00
return provider . deleteFolder ( folder )
2020-06-07 21:30:18 +00:00
}
// GetFolderByPath returns the folder with the specified path if any
2020-07-08 17:59:31 +00:00
func GetFolderByPath ( mappedPath string ) ( vfs . BaseVirtualFolder , error ) {
return provider . getFolderByPath ( mappedPath )
2020-06-07 21:30:18 +00:00
}
// GetFolders returns an array of folders respecting limit and offset
2020-07-08 17:59:31 +00:00
func GetFolders ( limit , offset int , order , folderPath string ) ( [ ] vfs . BaseVirtualFolder , error ) {
return provider . getFolders ( limit , offset , order , folderPath )
2020-06-07 21:30:18 +00:00
}
// DumpData returns all users and folders
2020-07-08 17:59:31 +00:00
func DumpData ( ) ( BackupData , error ) {
2020-06-07 21:30:18 +00:00
var data BackupData
2020-11-22 20:53:04 +00:00
data . Version = DumpVersion
2020-07-08 17:59:31 +00:00
users , err := provider . dumpUsers ( )
2020-06-07 21:30:18 +00:00
if err != nil {
return data , err
}
2020-07-08 17:59:31 +00:00
folders , err := provider . dumpFolders ( )
2020-06-07 21:30:18 +00:00
if err != nil {
return data , err
}
data . Users = users
data . Folders = folders
return data , err
}
2020-11-22 20:53:04 +00:00
// ParseDumpData tries to parse data as BackupData
func ParseDumpData ( data [ ] byte ) ( BackupData , error ) {
var dump BackupData
err := json . Unmarshal ( data , & dump )
if err == nil {
return dump , err
}
dump = BackupData { }
// try to parse as version 4
var dumpCompat backupDataV4Compat
err = json . Unmarshal ( data , & dumpCompat )
if err != nil {
return dump , err
}
logger . WarnToConsole ( "You are loading data from an old format, please update to the latest supported one. We only support the current and the previous format." )
providerLog ( logger . LevelWarn , "You are loading data from an old format, please update to the latest supported one. We only support the current and the previous format." )
dump . Folders = dumpCompat . Folders
for _ , compatUser := range dumpCompat . Users {
fsConfig , err := convertFsConfigFromV4 ( compatUser . FsConfig , compatUser . Username )
if err != nil {
return dump , err
}
dump . Users = append ( dump . Users , createUserFromV4 ( compatUser , fsConfig ) )
}
return dump , err
}
2019-11-14 17:48:01 +00:00
// GetProviderStatus returns an error if the provider is not available
2020-07-08 17:59:31 +00:00
func GetProviderStatus ( ) error {
return provider . checkAvailability ( )
2019-11-14 17:48:01 +00:00
}
2019-09-29 06:38:09 +00:00
// Close releases all provider resources.
// This method is used in test cases.
// Closing an uninitialized provider is not supported
2020-07-08 17:59:31 +00:00
func Close ( ) error {
2020-07-24 21:39:38 +00:00
if availabilityTicker != nil {
availabilityTicker . Stop ( )
availabilityTickerDone <- true
availabilityTicker = nil
}
2020-07-08 17:59:31 +00:00
return provider . close ( )
2019-09-28 20:48:52 +00:00
}
2020-02-08 13:44:25 +00:00
func createProvider ( basePath string ) error {
var err error
2020-06-07 21:30:18 +00:00
sqlPlaceholders = getSQLPlaceholders ( )
if err = validateSQLTablesPrefix ( ) ; err != nil {
return err
}
2020-02-08 13:44:25 +00:00
if config . Driver == SQLiteDataProviderName {
err = initializeSQLiteProvider ( basePath )
} else if config . Driver == PGSQLDataProviderName {
err = initializePGSQLProvider ( )
} else if config . Driver == MySQLDataProviderName {
err = initializeMySQLProvider ( )
} else if config . Driver == BoltDataProviderName {
err = initializeBoltProvider ( basePath )
} else if config . Driver == MemoryDataProviderName {
2020-11-25 08:18:36 +00:00
initializeMemoryProvider ( basePath )
2020-02-08 13:44:25 +00:00
} else {
err = fmt . Errorf ( "unsupported data provider: %v" , config . Driver )
}
return err
}
2019-09-28 21:44:36 +00:00
func buildUserHomeDir ( user * User ) {
2019-09-28 20:48:52 +00:00
if len ( user . HomeDir ) == 0 {
if len ( config . UsersBaseDir ) > 0 {
user . HomeDir = filepath . Join ( config . UsersBaseDir , user . Username )
}
}
2019-09-28 21:44:36 +00:00
}
2020-02-23 10:30:26 +00:00
func isVirtualDirOverlapped ( dir1 , dir2 string ) bool {
if dir1 == dir2 {
return true
}
if len ( dir1 ) > len ( dir2 ) {
if strings . HasPrefix ( dir1 , dir2 + "/" ) {
return true
}
}
if len ( dir2 ) > len ( dir1 ) {
if strings . HasPrefix ( dir2 , dir1 + "/" ) {
return true
}
}
return false
}
func isMappedDirOverlapped ( dir1 , dir2 string ) bool {
if dir1 == dir2 {
return true
}
if len ( dir1 ) > len ( dir2 ) {
if strings . HasPrefix ( dir1 , dir2 + string ( os . PathSeparator ) ) {
return true
}
}
if len ( dir2 ) > len ( dir1 ) {
if strings . HasPrefix ( dir2 , dir1 + string ( os . PathSeparator ) ) {
return true
}
}
return false
}
2020-06-07 21:30:18 +00:00
func validateFolderQuotaLimits ( folder vfs . VirtualFolder ) error {
if folder . QuotaSize < - 1 {
return & ValidationError { err : fmt . Sprintf ( "invalid quota_size: %v folder path %#v" , folder . QuotaSize , folder . MappedPath ) }
}
if folder . QuotaFiles < - 1 {
return & ValidationError { err : fmt . Sprintf ( "invalid quota_file: %v folder path %#v" , folder . QuotaSize , folder . MappedPath ) }
}
if ( folder . QuotaSize == - 1 && folder . QuotaFiles != - 1 ) || ( folder . QuotaFiles == - 1 && folder . QuotaSize != - 1 ) {
return & ValidationError { err : fmt . Sprintf ( "virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v" ,
folder . QuotaFiles , folder . QuotaSize ) }
}
return nil
}
func validateUserVirtualFolders ( user * User ) error {
2020-10-05 18:58:41 +00:00
if len ( user . VirtualFolders ) == 0 || user . FsConfig . Provider != LocalFilesystemProvider {
2020-02-23 10:30:26 +00:00
user . VirtualFolders = [ ] vfs . VirtualFolder { }
return nil
}
var virtualFolders [ ] vfs . VirtualFolder
mappedPaths := make ( map [ string ] string )
for _ , v := range user . VirtualFolders {
cleanedVPath := filepath . ToSlash ( path . Clean ( v . VirtualPath ) )
if ! path . IsAbs ( cleanedVPath ) || cleanedVPath == "/" {
return & ValidationError { err : fmt . Sprintf ( "invalid virtual folder %#v" , v . VirtualPath ) }
}
2020-06-07 21:30:18 +00:00
if err := validateFolderQuotaLimits ( v ) ; err != nil {
return err
}
2020-02-23 10:30:26 +00:00
cleanedMPath := filepath . Clean ( v . MappedPath )
if ! filepath . IsAbs ( cleanedMPath ) {
return & ValidationError { err : fmt . Sprintf ( "invalid mapped folder %#v" , v . MappedPath ) }
}
if isMappedDirOverlapped ( cleanedMPath , user . GetHomeDir ( ) ) {
return & ValidationError { err : fmt . Sprintf ( "invalid mapped folder %#v cannot be inside or contain the user home dir %#v" ,
v . MappedPath , user . GetHomeDir ( ) ) }
}
virtualFolders = append ( virtualFolders , vfs . VirtualFolder {
2020-06-07 21:30:18 +00:00
BaseVirtualFolder : vfs . BaseVirtualFolder {
MappedPath : cleanedMPath ,
} ,
VirtualPath : cleanedVPath ,
QuotaSize : v . QuotaSize ,
QuotaFiles : v . QuotaFiles ,
2020-02-23 10:30:26 +00:00
} )
for k , virtual := range mappedPaths {
2020-06-10 07:11:32 +00:00
if GetQuotaTracking ( ) > 0 {
if isMappedDirOverlapped ( k , cleanedMPath ) {
return & ValidationError { err : fmt . Sprintf ( "invalid mapped folder %#v overlaps with mapped folder %#v" ,
v . MappedPath , k ) }
}
} else {
if k == cleanedMPath {
return & ValidationError { err : fmt . Sprintf ( "duplicated mapped folder %#v" , v . MappedPath ) }
}
2020-02-23 10:30:26 +00:00
}
if isVirtualDirOverlapped ( virtual , cleanedVPath ) {
return & ValidationError { err : fmt . Sprintf ( "invalid virtual folder %#v overlaps with virtual folder %#v" ,
v . VirtualPath , virtual ) }
}
}
mappedPaths [ cleanedMPath ] = cleanedVPath
}
user . VirtualFolders = virtualFolders
return nil
}
2019-10-07 16:19:01 +00:00
func validatePermissions ( user * User ) error {
2019-12-29 22:27:32 +00:00
if len ( user . Permissions ) == 0 {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : "please grant some permissions to this user" }
2019-12-29 22:27:32 +00:00
}
2019-12-25 17:20:19 +00:00
permissions := make ( map [ string ] [ ] string )
if _ , ok := user . Permissions [ "/" ] ; ! ok {
2020-04-30 13:06:15 +00:00
return & ValidationError { err : "permissions for the root dir \"/\" must be set" }
2019-10-07 16:19:01 +00:00
}
2019-12-25 17:20:19 +00:00
for dir , perms := range user . Permissions {
2020-02-10 18:28:35 +00:00
if len ( perms ) == 0 && dir == "/" {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : fmt . Sprintf ( "no permissions granted for the directory: %#v" , dir ) }
2019-12-25 17:20:19 +00:00
}
2020-02-19 21:39:30 +00:00
if len ( perms ) > len ( ValidPerms ) {
return & ValidationError { err : "invalid permissions" }
}
2019-12-25 17:20:19 +00:00
for _ , p := range perms {
if ! utils . IsStringInSlice ( p , ValidPerms ) {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : fmt . Sprintf ( "invalid permission: %#v" , p ) }
2019-12-25 17:20:19 +00:00
}
}
cleanedDir := filepath . ToSlash ( path . Clean ( dir ) )
if cleanedDir != "/" {
cleanedDir = strings . TrimSuffix ( cleanedDir , "/" )
}
if ! path . IsAbs ( cleanedDir ) {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : fmt . Sprintf ( "cannot set permissions for non absolute path: %#v" , dir ) }
2019-12-25 17:20:19 +00:00
}
2020-02-10 18:28:35 +00:00
if dir != cleanedDir && cleanedDir == "/" {
return & ValidationError { err : fmt . Sprintf ( "cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"" , dir ) }
}
2019-12-25 17:20:19 +00:00
if utils . IsStringInSlice ( PermAny , perms ) {
permissions [ cleanedDir ] = [ ] string { PermAny }
} else {
permissions [ cleanedDir ] = perms
}
2019-10-07 16:19:01 +00:00
}
2019-12-25 17:20:19 +00:00
user . Permissions = permissions
2019-10-07 16:19:01 +00:00
return nil
}
2019-12-30 17:37:50 +00:00
func validatePublicKeys ( user * User ) error {
if len ( user . PublicKeys ) == 0 {
user . PublicKeys = [ ] string { }
}
for i , k := range user . PublicKeys {
_ , _ , _ , _ , err := ssh . ParseAuthorizedKey ( [ ] byte ( k ) )
if err != nil {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : fmt . Sprintf ( "could not parse key nr. %d: %s" , i , err ) }
2019-12-30 17:37:50 +00:00
}
}
return nil
}
2020-11-15 21:04:48 +00:00
func validateFiltersPatternExtensions ( user * User ) error {
if len ( user . Filters . FilePatterns ) == 0 {
user . Filters . FilePatterns = [ ] PatternsFilter { }
return nil
}
filteredPaths := [ ] string { }
var filters [ ] PatternsFilter
for _ , f := range user . Filters . FilePatterns {
cleanedPath := filepath . ToSlash ( path . Clean ( f . Path ) )
if ! path . IsAbs ( cleanedPath ) {
return & ValidationError { err : fmt . Sprintf ( "invalid path %#v for file patterns filter" , f . Path ) }
}
if utils . IsStringInSlice ( cleanedPath , filteredPaths ) {
return & ValidationError { err : fmt . Sprintf ( "duplicate file patterns filter for path %#v" , f . Path ) }
}
if len ( f . AllowedPatterns ) == 0 && len ( f . DeniedPatterns ) == 0 {
return & ValidationError { err : fmt . Sprintf ( "empty file patterns filter for path %#v" , f . Path ) }
}
f . Path = cleanedPath
allowed := make ( [ ] string , 0 , len ( f . AllowedPatterns ) )
denied := make ( [ ] string , 0 , len ( f . DeniedPatterns ) )
for _ , pattern := range f . AllowedPatterns {
_ , err := path . Match ( pattern , "abc" )
if err != nil {
2020-11-16 18:21:50 +00:00
return & ValidationError { err : fmt . Sprintf ( "invalid file pattern filter %#v" , pattern ) }
2020-11-15 21:04:48 +00:00
}
allowed = append ( allowed , strings . ToLower ( pattern ) )
}
for _ , pattern := range f . DeniedPatterns {
_ , err := path . Match ( pattern , "abc" )
if err != nil {
2020-11-16 18:21:50 +00:00
return & ValidationError { err : fmt . Sprintf ( "invalid file pattern filter %#v" , pattern ) }
2020-11-15 21:04:48 +00:00
}
denied = append ( denied , strings . ToLower ( pattern ) )
}
f . AllowedPatterns = allowed
f . DeniedPatterns = denied
filters = append ( filters , f )
filteredPaths = append ( filteredPaths , cleanedPath )
}
user . Filters . FilePatterns = filters
return nil
}
2020-03-01 21:10:29 +00:00
func validateFiltersFileExtensions ( user * User ) error {
if len ( user . Filters . FileExtensions ) == 0 {
user . Filters . FileExtensions = [ ] ExtensionsFilter { }
return nil
}
filteredPaths := [ ] string { }
var filters [ ] ExtensionsFilter
for _ , f := range user . Filters . FileExtensions {
cleanedPath := filepath . ToSlash ( path . Clean ( f . Path ) )
if ! path . IsAbs ( cleanedPath ) {
return & ValidationError { err : fmt . Sprintf ( "invalid path %#v for file extensions filter" , f . Path ) }
}
if utils . IsStringInSlice ( cleanedPath , filteredPaths ) {
return & ValidationError { err : fmt . Sprintf ( "duplicate file extensions filter for path %#v" , f . Path ) }
}
if len ( f . AllowedExtensions ) == 0 && len ( f . DeniedExtensions ) == 0 {
return & ValidationError { err : fmt . Sprintf ( "empty file extensions filter for path %#v" , f . Path ) }
}
f . Path = cleanedPath
2020-11-15 21:04:48 +00:00
allowed := make ( [ ] string , 0 , len ( f . AllowedExtensions ) )
denied := make ( [ ] string , 0 , len ( f . DeniedExtensions ) )
for _ , ext := range f . AllowedExtensions {
allowed = append ( allowed , strings . ToLower ( ext ) )
}
for _ , ext := range f . DeniedExtensions {
denied = append ( denied , strings . ToLower ( ext ) )
}
f . AllowedExtensions = allowed
f . DeniedExtensions = denied
2020-03-01 21:10:29 +00:00
filters = append ( filters , f )
filteredPaths = append ( filteredPaths , cleanedPath )
}
user . Filters . FileExtensions = filters
return nil
}
2020-11-15 21:04:48 +00:00
func validateFileFilters ( user * User ) error {
if err := validateFiltersFileExtensions ( user ) ; err != nil {
return err
}
return validateFiltersPatternExtensions ( user )
}
2019-12-30 17:37:50 +00:00
func validateFilters ( user * User ) error {
if len ( user . Filters . AllowedIP ) == 0 {
user . Filters . AllowedIP = [ ] string { }
}
if len ( user . Filters . DeniedIP ) == 0 {
user . Filters . DeniedIP = [ ] string { }
}
2020-02-19 21:39:30 +00:00
if len ( user . Filters . DeniedLoginMethods ) == 0 {
user . Filters . DeniedLoginMethods = [ ] string { }
}
2020-08-17 10:49:20 +00:00
if len ( user . Filters . DeniedProtocols ) == 0 {
user . Filters . DeniedProtocols = [ ] string { }
}
2019-12-30 17:37:50 +00:00
for _ , IPMask := range user . Filters . DeniedIP {
_ , _ , err := net . ParseCIDR ( IPMask )
if err != nil {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : fmt . Sprintf ( "could not parse denied IP/Mask %#v : %v" , IPMask , err ) }
2019-12-30 17:37:50 +00:00
}
}
for _ , IPMask := range user . Filters . AllowedIP {
_ , _ , err := net . ParseCIDR ( IPMask )
if err != nil {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : fmt . Sprintf ( "could not parse allowed IP/Mask %#v : %v" , IPMask , err ) }
2019-12-30 17:37:50 +00:00
}
}
2020-02-19 21:39:30 +00:00
if len ( user . Filters . DeniedLoginMethods ) >= len ( ValidSSHLoginMethods ) {
return & ValidationError { err : "invalid denied_login_methods" }
}
for _ , loginMethod := range user . Filters . DeniedLoginMethods {
if ! utils . IsStringInSlice ( loginMethod , ValidSSHLoginMethods ) {
return & ValidationError { err : fmt . Sprintf ( "invalid login method: %#v" , loginMethod ) }
}
}
2020-08-17 10:49:20 +00:00
if len ( user . Filters . DeniedProtocols ) >= len ( ValidProtocols ) {
return & ValidationError { err : "invalid denied_protocols" }
2020-03-01 21:10:29 +00:00
}
2020-08-17 10:49:20 +00:00
for _ , p := range user . Filters . DeniedProtocols {
if ! utils . IsStringInSlice ( p , ValidProtocols ) {
return & ValidationError { err : fmt . Sprintf ( "invalid protocol: %#v" , p ) }
}
}
2020-11-15 21:04:48 +00:00
return validateFileFilters ( user )
2019-12-30 17:37:50 +00:00
}
2020-01-31 18:04:00 +00:00
func saveGCSCredentials ( user * User ) error {
2020-10-05 18:58:41 +00:00
if user . FsConfig . Provider != GCSFilesystemProvider {
2020-01-31 18:04:00 +00:00
return nil
}
2020-11-22 20:53:04 +00:00
if user . FsConfig . GCSConfig . Credentials . Payload == "" {
2020-01-31 18:04:00 +00:00
return nil
}
2020-10-22 08:42:40 +00:00
if config . PreferDatabaseCredentials {
2020-11-22 20:53:04 +00:00
if user . FsConfig . GCSConfig . Credentials . IsPlain ( ) {
user . FsConfig . GCSConfig . Credentials . AdditionalData = user . Username
err := user . FsConfig . GCSConfig . Credentials . Encrypt ( )
if err != nil {
return err
}
}
2020-10-22 08:42:40 +00:00
return nil
2020-01-31 18:04:00 +00:00
}
2020-11-22 20:53:04 +00:00
if user . FsConfig . GCSConfig . Credentials . IsPlain ( ) {
user . FsConfig . GCSConfig . Credentials . AdditionalData = user . Username
err := user . FsConfig . GCSConfig . Credentials . Encrypt ( )
if err != nil {
return & ValidationError { err : fmt . Sprintf ( "could not encrypt GCS credentials: %v" , err ) }
}
}
creds , err := json . Marshal ( user . FsConfig . GCSConfig . Credentials )
if err != nil {
return & ValidationError { err : fmt . Sprintf ( "could not marshal GCS credentials: %v" , err ) }
}
err = ioutil . WriteFile ( user . getGCSCredentialsFilePath ( ) , creds , 0600 )
2020-01-31 18:04:00 +00:00
if err != nil {
return & ValidationError { err : fmt . Sprintf ( "could not save GCS credentials: %v" , err ) }
}
2020-11-22 20:53:04 +00:00
user . FsConfig . GCSConfig . Credentials = vfs . Secret { }
2020-01-31 18:04:00 +00:00
return nil
}
2020-01-19 06:41:05 +00:00
func validateFilesystemConfig ( user * User ) error {
2020-10-05 18:58:41 +00:00
if user . FsConfig . Provider == S3FilesystemProvider {
2020-01-19 06:41:05 +00:00
err := vfs . ValidateS3FsConfig ( & user . FsConfig . S3Config )
if err != nil {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : fmt . Sprintf ( "could not validate s3config: %v" , err ) }
2020-01-19 06:41:05 +00:00
}
2020-11-22 20:53:04 +00:00
if user . FsConfig . S3Config . AccessSecret . IsPlain ( ) {
user . FsConfig . S3Config . AccessSecret . AdditionalData = user . Username
err = user . FsConfig . S3Config . AccessSecret . Encrypt ( )
if err != nil {
return & ValidationError { err : fmt . Sprintf ( "could not encrypt s3 access secret: %v" , err ) }
2020-01-19 06:41:05 +00:00
}
}
2020-11-22 20:53:04 +00:00
user . FsConfig . GCSConfig = vfs . GCSFsConfig { }
user . FsConfig . AzBlobConfig = vfs . AzBlobFsConfig { }
2020-01-19 06:41:05 +00:00
return nil
2020-10-05 18:58:41 +00:00
} else if user . FsConfig . Provider == GCSFilesystemProvider {
2020-01-31 18:04:00 +00:00
err := vfs . ValidateGCSFsConfig ( & user . FsConfig . GCSConfig , user . getGCSCredentialsFilePath ( ) )
if err != nil {
return & ValidationError { err : fmt . Sprintf ( "could not validate GCS config: %v" , err ) }
}
2020-11-22 20:53:04 +00:00
user . FsConfig . S3Config = vfs . S3FsConfig { }
user . FsConfig . AzBlobConfig = vfs . AzBlobFsConfig { }
2020-01-31 18:04:00 +00:00
return nil
2020-10-25 07:18:48 +00:00
} else if user . FsConfig . Provider == AzureBlobFilesystemProvider {
err := vfs . ValidateAzBlobFsConfig ( & user . FsConfig . AzBlobConfig )
if err != nil {
return & ValidationError { err : fmt . Sprintf ( "could not validate Azure Blob config: %v" , err ) }
}
2020-11-22 20:53:04 +00:00
if user . FsConfig . AzBlobConfig . AccountKey . IsPlain ( ) {
user . FsConfig . AzBlobConfig . AccountKey . AdditionalData = user . Username
err = user . FsConfig . AzBlobConfig . AccountKey . Encrypt ( )
if err != nil {
return & ValidationError { err : fmt . Sprintf ( "could not encrypt Azure blob account key: %v" , err ) }
2020-10-25 07:18:48 +00:00
}
}
2020-11-22 20:53:04 +00:00
user . FsConfig . S3Config = vfs . S3FsConfig { }
user . FsConfig . GCSConfig = vfs . GCSFsConfig { }
2020-10-25 07:18:48 +00:00
return nil
2020-01-19 06:41:05 +00:00
}
2020-10-05 18:58:41 +00:00
user . FsConfig . Provider = LocalFilesystemProvider
2020-01-19 06:41:05 +00:00
user . FsConfig . S3Config = vfs . S3FsConfig { }
2020-01-31 18:04:00 +00:00
user . FsConfig . GCSConfig = vfs . GCSFsConfig { }
2020-10-25 07:18:48 +00:00
user . FsConfig . AzBlobConfig = vfs . AzBlobFsConfig { }
2020-01-19 06:41:05 +00:00
return nil
}
2020-01-31 22:26:56 +00:00
func validateBaseParams ( user * User ) error {
2020-09-18 17:21:24 +00:00
if user . Username == "" {
return & ValidationError { err : "username is mandatory" }
2019-07-20 10:26:52 +00:00
}
2020-09-18 17:21:24 +00:00
if user . HomeDir == "" {
return & ValidationError { err : "home_dir is mandatory" }
}
if user . Password == "" && len ( user . PublicKeys ) == 0 {
2020-01-19 22:23:09 +00:00
return & ValidationError { err : "please set a password or at least a public_key" }
2019-07-20 10:26:52 +00:00
}
if ! filepath . IsAbs ( user . HomeDir ) {
return & ValidationError { err : fmt . Sprintf ( "home_dir must be an absolute path, actual value: %v" , user . HomeDir ) }
}
2020-01-31 22:26:56 +00:00
return nil
}
2020-03-22 13:03:06 +00:00
func createUserPasswordHash ( user * User ) error {
if len ( user . Password ) > 0 && ! utils . IsStringPrefixInSlice ( user . Password , hashPwdPrefixes ) {
2020-09-04 15:09:31 +00:00
pwd , err := argon2id . CreateHash ( user . Password , argon2Params )
2020-03-22 13:03:06 +00:00
if err != nil {
return err
}
user . Password = pwd
}
return nil
}
2020-06-07 21:30:18 +00:00
func validateFolder ( folder * vfs . BaseVirtualFolder ) error {
cleanedMPath := filepath . Clean ( folder . MappedPath )
if ! filepath . IsAbs ( cleanedMPath ) {
return & ValidationError { err : fmt . Sprintf ( "invalid mapped folder %#v" , folder . MappedPath ) }
}
folder . MappedPath = cleanedMPath
return nil
}
2020-01-31 22:26:56 +00:00
func validateUser ( user * User ) error {
buildUserHomeDir ( user )
if err := validateBaseParams ( user ) ; err != nil {
return err
}
2019-10-07 16:19:01 +00:00
if err := validatePermissions ( user ) ; err != nil {
return err
2019-07-20 10:26:52 +00:00
}
2020-01-19 06:41:05 +00:00
if err := validateFilesystemConfig ( user ) ; err != nil {
return err
}
2020-06-07 21:30:18 +00:00
if err := validateUserVirtualFolders ( user ) ; err != nil {
2020-02-23 10:30:26 +00:00
return err
}
2019-11-13 10:36:21 +00:00
if user . Status < 0 || user . Status > 1 {
return & ValidationError { err : fmt . Sprintf ( "invalid user status: %v" , user . Status ) }
}
2020-03-22 13:03:06 +00:00
if err := createUserPasswordHash ( user ) ; err != nil {
return err
2019-07-20 10:26:52 +00:00
}
2019-12-30 17:37:50 +00:00
if err := validatePublicKeys ( user ) ; err != nil {
return err
2019-12-29 16:21:25 +00:00
}
2019-12-30 17:37:50 +00:00
if err := validateFilters ( user ) ; err != nil {
return err
2019-07-20 10:26:52 +00:00
}
2020-01-31 18:04:00 +00:00
if err := saveGCSCredentials ( user ) ; err != nil {
return err
}
2019-07-20 10:26:52 +00:00
return nil
}
2019-11-13 10:36:21 +00:00
func checkLoginConditions ( user User ) error {
if user . Status < 1 {
return fmt . Errorf ( "user %#v is disabled" , user . Username )
}
if user . ExpirationDate > 0 && user . ExpirationDate < utils . GetTimeAsMsSinceEpoch ( time . Now ( ) ) {
return fmt . Errorf ( "user %#v is expired, expiration timestamp: %v current timestamp: %v" , user . Username ,
user . ExpirationDate , utils . GetTimeAsMsSinceEpoch ( time . Now ( ) ) )
}
return nil
}
2020-08-19 17:36:12 +00:00
func isPasswordOK ( user * User , password string ) ( bool , error ) {
2019-12-29 06:43:59 +00:00
match := false
2020-08-19 17:36:12 +00:00
var err error
2019-08-12 16:31:31 +00:00
if strings . HasPrefix ( user . Password , argonPwdPrefix ) {
match , err = argon2id . ComparePasswordAndHash ( password , user . Password )
if err != nil {
2019-09-06 13:19:01 +00:00
providerLog ( logger . LevelWarn , "error comparing password with argon hash: %v" , err )
2020-08-19 17:36:12 +00:00
return match , err
2019-08-12 16:31:31 +00:00
}
} else if strings . HasPrefix ( user . Password , bcryptPwdPrefix ) {
if err = bcrypt . CompareHashAndPassword ( [ ] byte ( user . Password ) , [ ] byte ( password ) ) ; err != nil {
2019-09-06 13:19:01 +00:00
providerLog ( logger . LevelWarn , "error comparing password with bcrypt hash: %v" , err )
2020-08-19 17:36:12 +00:00
return match , err
2019-08-12 16:31:31 +00:00
}
match = true
2019-08-17 13:20:49 +00:00
} else if utils . IsStringPrefixInSlice ( user . Password , pbkdfPwdPrefixes ) {
match , err = comparePbkdf2PasswordAndHash ( password , user . Password )
if err != nil {
2020-08-19 17:36:12 +00:00
return match , err
2019-08-17 13:20:49 +00:00
}
2019-12-29 06:43:59 +00:00
} else if utils . IsStringPrefixInSlice ( user . Password , unixPwdPrefixes ) {
match , err = compareUnixPasswordAndHash ( user , password )
if err != nil {
2020-08-19 17:36:12 +00:00
return match , err
2019-09-15 06:34:44 +00:00
}
2019-08-12 16:31:31 +00:00
}
2020-08-19 17:36:12 +00:00
return match , err
}
func checkUserAndPass ( user User , password , ip , protocol string ) ( User , error ) {
err := checkLoginConditions ( user )
if err != nil {
return user , err
}
if len ( user . Password ) == 0 {
return user , errors . New ( "Credentials cannot be null or empty" )
}
hookResponse , err := executeCheckPasswordHook ( user . Username , password , ip , protocol )
if err != nil {
providerLog ( logger . LevelDebug , "error executing check password hook: %v" , err )
return user , errors . New ( "Unable to check credentials" )
}
switch hookResponse . Status {
case - 1 :
// no hook configured
case 1 :
providerLog ( logger . LevelDebug , "password accepted by check password hook" )
return user , nil
case 2 :
providerLog ( logger . LevelDebug , "partial success from check password hook" )
password = hookResponse . ToVerify
default :
providerLog ( logger . LevelDebug , "password rejected by check password hook, status: %v" , hookResponse . Status )
2020-08-31 17:25:17 +00:00
return user , ErrInvalidCredentials
2020-08-19 17:36:12 +00:00
}
match , err := isPasswordOK ( & user , password )
2019-08-12 16:31:31 +00:00
if ! match {
2020-08-31 17:25:17 +00:00
err = ErrInvalidCredentials
2019-08-12 16:31:31 +00:00
}
return user , err
}
2020-04-09 21:32:42 +00:00
func checkUserAndPubKey ( user User , pubKey [ ] byte ) ( User , string , error ) {
2019-11-13 10:36:21 +00:00
err := checkLoginConditions ( user )
if err != nil {
return user , "" , err
}
2019-08-12 16:31:31 +00:00
if len ( user . PublicKeys ) == 0 {
2020-08-31 17:25:17 +00:00
return user , "" , ErrInvalidCredentials
2019-08-12 16:31:31 +00:00
}
for i , k := range user . PublicKeys {
2019-09-05 19:35:53 +00:00
storedPubKey , comment , _ , _ , err := ssh . ParseAuthorizedKey ( [ ] byte ( k ) )
2019-08-12 16:31:31 +00:00
if err != nil {
2019-09-06 13:19:01 +00:00
providerLog ( logger . LevelWarn , "error parsing stored public key %d for user %v: %v" , i , user . Username , err )
2019-09-05 19:35:53 +00:00
return user , "" , err
2019-08-12 16:31:31 +00:00
}
2020-04-09 21:32:42 +00:00
if bytes . Equal ( storedPubKey . Marshal ( ) , pubKey ) {
2020-05-15 18:08:53 +00:00
certInfo := ""
cert , ok := storedPubKey . ( * ssh . Certificate )
if ok {
certInfo = fmt . Sprintf ( " %v ID: %v Serial: %v CA: %v" , cert . Type ( ) , cert . KeyId , cert . Serial ,
ssh . FingerprintSHA256 ( cert . SignatureKey ) )
}
return user , fmt . Sprintf ( "%v:%v%v" , ssh . FingerprintSHA256 ( storedPubKey ) , comment , certInfo ) , nil
2019-08-12 16:31:31 +00:00
}
}
2020-08-31 17:25:17 +00:00
return user , "" , ErrInvalidCredentials
2019-08-12 16:31:31 +00:00
}
2020-08-19 17:36:12 +00:00
func compareUnixPasswordAndHash ( user * User , password string ) ( bool , error ) {
2020-09-04 19:08:09 +00:00
var crypter crypt . Crypter
2019-12-29 06:43:59 +00:00
if strings . HasPrefix ( user . Password , sha512cryptPwdPrefix ) {
2020-09-04 19:08:09 +00:00
crypter = sha512_crypt . New ( )
} else if strings . HasPrefix ( user . Password , md5cryptPwdPrefix ) {
crypter = md5_crypt . New ( )
} else if strings . HasPrefix ( user . Password , md5cryptApr1PwdPrefix ) {
crypter = apr1_crypt . New ( )
2019-12-29 06:43:59 +00:00
} else {
2020-09-04 19:08:09 +00:00
return false , errors . New ( "unix crypt: invalid or unsupported hash format" )
2019-12-29 06:43:59 +00:00
}
2020-09-04 19:08:09 +00:00
if err := crypter . Verify ( user . Password , [ ] byte ( password ) ) ; err != nil {
return false , err
}
return true , nil
2019-12-29 06:43:59 +00:00
}
2019-08-17 13:20:49 +00:00
func comparePbkdf2PasswordAndHash ( password , hashedPassword string ) ( bool , error ) {
vals := strings . Split ( hashedPassword , "$" )
if len ( vals ) != 5 {
return false , fmt . Errorf ( "pbkdf2: hash is not in the correct format" )
}
2020-04-11 10:25:21 +00:00
iterations , err := strconv . Atoi ( vals [ 2 ] )
if err != nil {
return false , err
}
expected , err := base64 . StdEncoding . DecodeString ( vals [ 4 ] )
if err != nil {
return false , err
}
var salt [ ] byte
if utils . IsStringPrefixInSlice ( hashedPassword , pbkdfPwdB64SaltPrefixes ) {
salt , err = base64 . StdEncoding . DecodeString ( vals [ 3 ] )
if err != nil {
return false , err
}
} else {
salt = [ ] byte ( vals [ 3 ] )
}
2019-08-17 13:20:49 +00:00
var hashFunc func ( ) hash . Hash
2020-04-11 10:25:21 +00:00
if strings . HasPrefix ( hashedPassword , pbkdf2SHA256Prefix ) || strings . HasPrefix ( hashedPassword , pbkdf2SHA256B64SaltPrefix ) {
2019-08-17 13:20:49 +00:00
hashFunc = sha256 . New
} else if strings . HasPrefix ( hashedPassword , pbkdf2SHA512Prefix ) {
hashFunc = sha512 . New
} else if strings . HasPrefix ( hashedPassword , pbkdf2SHA1Prefix ) {
hashFunc = sha1 . New
} else {
return false , fmt . Errorf ( "pbkdf2: invalid or unsupported hash format %v" , vals [ 1 ] )
}
2020-04-11 10:25:21 +00:00
df := pbkdf2 . Key ( [ ] byte ( password ) , salt , iterations , len ( expected ) , hashFunc )
2020-03-28 15:09:06 +00:00
return subtle . ConstantTimeCompare ( df , expected ) == 1 , nil
2019-08-17 13:20:49 +00:00
}
2020-01-31 18:04:00 +00:00
func addCredentialsToUser ( user * User ) error {
2020-10-05 18:58:41 +00:00
if user . FsConfig . Provider != GCSFilesystemProvider {
2020-01-31 18:04:00 +00:00
return nil
}
2020-02-19 08:41:15 +00:00
if user . FsConfig . GCSConfig . AutomaticCredentials > 0 {
return nil
}
2020-10-22 08:42:40 +00:00
// Don't read from file if credentials have already been set
2020-11-22 20:53:04 +00:00
if user . FsConfig . GCSConfig . Credentials . IsValid ( ) {
2020-10-22 08:42:40 +00:00
return nil
}
2020-01-31 18:04:00 +00:00
cred , err := ioutil . ReadFile ( user . getGCSCredentialsFilePath ( ) )
if err != nil {
return err
}
2020-11-22 20:53:04 +00:00
return json . Unmarshal ( cred , & user . FsConfig . GCSConfig . Credentials )
2020-01-31 18:04:00 +00:00
}
2019-07-20 10:26:52 +00:00
func getSSLMode ( ) string {
2019-09-06 09:23:06 +00:00
if config . Driver == PGSQLDataProviderName {
2019-07-20 10:26:52 +00:00
if config . SSLMode == 0 {
return "disable"
} else if config . SSLMode == 1 {
return "require"
} else if config . SSLMode == 2 {
return "verify-ca"
} else if config . SSLMode == 3 {
return "verify-full"
}
} else if config . Driver == MySQLDataProviderName {
if config . SSLMode == 0 {
return "false"
} else if config . SSLMode == 1 {
return "true"
} else if config . SSLMode == 2 {
return "skip-verify"
} else if config . SSLMode == 3 {
return "preferred"
}
}
return ""
}
2019-09-06 13:19:01 +00:00
2019-09-13 16:45:36 +00:00
func startAvailabilityTimer ( ) {
2020-07-24 21:39:38 +00:00
availabilityTicker = time . NewTicker ( 30 * time . Second )
2019-09-28 20:48:52 +00:00
availabilityTickerDone = make ( chan bool )
2019-09-13 16:45:36 +00:00
checkDataprovider ( )
go func ( ) {
2019-09-28 20:48:52 +00:00
for {
select {
case <- availabilityTickerDone :
return
case <- availabilityTicker . C :
checkDataprovider ( )
}
2019-09-13 16:45:36 +00:00
}
} ( )
}
2020-11-22 20:53:04 +00:00
func validateCredentialsDir ( basePath string , preferDbCredentials bool ) error {
2020-01-31 18:04:00 +00:00
if filepath . IsAbs ( config . CredentialsPath ) {
credentialsDirPath = config . CredentialsPath
} else {
credentialsDirPath = filepath . Join ( basePath , config . CredentialsPath )
}
2020-11-22 20:53:04 +00:00
// if we want to store credentials inside the database just stop here
// we just populate credentialsDirPath to be able to use existing users
// with credential files
if preferDbCredentials {
return nil
}
2020-01-31 18:04:00 +00:00
fi , err := os . Stat ( credentialsDirPath )
if err == nil {
if ! fi . IsDir ( ) {
return errors . New ( "Credential path is not a valid directory" )
}
return nil
}
if ! os . IsNotExist ( err ) {
return err
}
return os . MkdirAll ( credentialsDirPath , 0700 )
}
2019-09-13 16:45:36 +00:00
func checkDataprovider ( ) {
err := provider . checkAvailability ( )
if err != nil {
providerLog ( logger . LevelWarn , "check availability error: %v" , err )
}
metrics . UpdateDataProviderAvailability ( err )
}
2020-01-21 09:54:05 +00:00
func terminateInteractiveAuthProgram ( cmd * exec . Cmd , isFinished bool ) {
if isFinished {
return
}
providerLog ( logger . LevelInfo , "kill interactive auth program after an unexpected error" )
2020-04-30 12:23:55 +00:00
err := cmd . Process . Kill ( )
if err != nil {
providerLog ( logger . LevelDebug , "error killing interactive auth program: %v" , err )
}
2020-01-21 09:54:05 +00:00
}
2020-04-01 21:25:23 +00:00
func validateKeyboardAuthResponse ( response keyboardAuthHookResponse ) error {
if len ( response . Questions ) == 0 {
err := errors . New ( "interactive auth error: hook response does not contain questions" )
providerLog ( logger . LevelInfo , "%v" , err )
return err
}
if len ( response . Questions ) != len ( response . Echos ) {
err := fmt . Errorf ( "interactive auth error, http hook response questions don't match echos: %v %v" ,
len ( response . Questions ) , len ( response . Echos ) )
providerLog ( logger . LevelInfo , "%v" , err )
return err
}
return nil
}
func sendKeyboardAuthHTTPReq ( url * url . URL , request keyboardAuthHookRequest ) ( keyboardAuthHookResponse , error ) {
var response keyboardAuthHookResponse
2020-04-26 21:29:09 +00:00
httpClient := httpclient . GetHTTPClient ( )
2020-04-01 21:25:23 +00:00
reqAsJSON , err := json . Marshal ( request )
if err != nil {
providerLog ( logger . LevelWarn , "error serializing keyboard interactive auth request: %v" , err )
return response , err
}
resp , err := httpClient . Post ( url . String ( ) , "application/json" , bytes . NewBuffer ( reqAsJSON ) )
if err != nil {
providerLog ( logger . LevelWarn , "error getting keyboard interactive auth hook HTTP response: %v" , err )
return response , err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return response , fmt . Errorf ( "wrong keyboard interactive auth http status code: %v, expected 200" , resp . StatusCode )
}
err = render . DecodeJSON ( resp . Body , & response )
return response , err
}
2020-08-19 17:36:12 +00:00
func executeKeyboardInteractiveHTTPHook ( user User , authHook string , client ssh . KeyboardInteractiveChallenge , ip , protocol string ) ( int , error ) {
2020-04-01 21:25:23 +00:00
authResult := 0
var url * url . URL
url , err := url . Parse ( authHook )
if err != nil {
providerLog ( logger . LevelWarn , "invalid url for keyboard interactive hook %#v, error: %v" , authHook , err )
return authResult , err
}
requestID := xid . New ( ) . String ( )
req := keyboardAuthHookRequest {
Username : user . Username ,
2020-08-04 16:03:28 +00:00
IP : ip ,
2020-04-01 21:25:23 +00:00
Password : user . Password ,
RequestID : requestID ,
}
var response keyboardAuthHookResponse
for {
response , err = sendKeyboardAuthHTTPReq ( url , req )
if err != nil {
return authResult , err
}
if response . AuthResult != 0 {
return response . AuthResult , err
}
if err = validateKeyboardAuthResponse ( response ) ; err != nil {
return authResult , err
}
2020-08-19 17:36:12 +00:00
answers , err := getKeyboardInteractiveAnswers ( client , response , user , ip , protocol )
2020-04-01 21:25:23 +00:00
if err != nil {
return authResult , err
}
req = keyboardAuthHookRequest {
RequestID : requestID ,
Username : user . Username ,
Password : user . Password ,
Answers : answers ,
Questions : response . Questions ,
}
}
}
func getKeyboardInteractiveAnswers ( client ssh . KeyboardInteractiveChallenge , response keyboardAuthHookResponse ,
2020-08-19 17:36:12 +00:00
user User , ip , protocol string ) ( [ ] string , error ) {
2020-02-16 10:43:52 +00:00
questions := response . Questions
answers , err := client ( user . Username , response . Instruction , questions , response . Echos )
if err != nil {
providerLog ( logger . LevelInfo , "error getting interactive auth client response: %v" , err )
2020-04-01 21:25:23 +00:00
return answers , err
2020-02-16 10:43:52 +00:00
}
if len ( answers ) != len ( questions ) {
err = fmt . Errorf ( "client answers does not match questions, expected: %v actual: %v" , questions , answers )
providerLog ( logger . LevelInfo , "keyboard interactive auth error: %v" , err )
2020-04-01 21:25:23 +00:00
return answers , err
2020-02-16 10:43:52 +00:00
}
if len ( answers ) == 1 && response . CheckPwd > 0 {
2020-08-19 17:36:12 +00:00
_ , err = checkUserAndPass ( user , answers [ 0 ] , ip , protocol )
2020-04-01 21:25:23 +00:00
providerLog ( logger . LevelInfo , "interactive auth hook requested password validation for user %#v, validation error: %v" ,
2020-02-16 10:43:52 +00:00
user . Username , err )
if err != nil {
2020-04-01 21:25:23 +00:00
return answers , err
2020-02-16 10:43:52 +00:00
}
answers [ 0 ] = "OK"
}
2020-04-01 21:25:23 +00:00
return answers , err
}
func handleProgramInteractiveQuestions ( client ssh . KeyboardInteractiveChallenge , response keyboardAuthHookResponse ,
2020-08-19 17:36:12 +00:00
user User , stdin io . WriteCloser , ip , protocol string ) error {
answers , err := getKeyboardInteractiveAnswers ( client , response , user , ip , protocol )
2020-04-01 21:25:23 +00:00
if err != nil {
return err
}
2020-02-16 10:43:52 +00:00
for _ , answer := range answers {
if runtime . GOOS == "windows" {
answer += "\r"
}
answer += "\n"
_ , err = stdin . Write ( [ ] byte ( answer ) )
if err != nil {
providerLog ( logger . LevelError , "unable to write client answer to keyboard interactive program: %v" , err )
return err
}
}
return nil
}
2020-08-19 17:36:12 +00:00
func executeKeyboardInteractiveProgram ( user User , authHook string , client ssh . KeyboardInteractiveChallenge , ip , protocol string ) ( int , error ) {
2020-04-01 21:25:23 +00:00
authResult := 0
2020-01-21 09:54:05 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 60 * time . Second )
defer cancel ( )
2020-04-01 21:25:23 +00:00
cmd := exec . CommandContext ( ctx , authHook )
2020-01-21 09:54:05 +00:00
cmd . Env = append ( os . Environ ( ) ,
fmt . Sprintf ( "SFTPGO_AUTHD_USERNAME=%v" , user . Username ) ,
2020-08-04 16:03:28 +00:00
fmt . Sprintf ( "SFTPGO_AUTHD_IP=%v" , ip ) ,
2020-01-21 09:54:05 +00:00
fmt . Sprintf ( "SFTPGO_AUTHD_PASSWORD=%v" , user . Password ) )
stdout , err := cmd . StdoutPipe ( )
if err != nil {
2020-04-01 21:25:23 +00:00
return authResult , err
2020-01-21 09:54:05 +00:00
}
stdin , err := cmd . StdinPipe ( )
if err != nil {
2020-04-01 21:25:23 +00:00
return authResult , err
2020-01-21 09:54:05 +00:00
}
err = cmd . Start ( )
if err != nil {
2020-04-01 21:25:23 +00:00
return authResult , err
2020-01-21 09:54:05 +00:00
}
var once sync . Once
scanner := bufio . NewScanner ( stdout )
for scanner . Scan ( ) {
2020-04-01 21:25:23 +00:00
var response keyboardAuthHookResponse
err = json . Unmarshal ( scanner . Bytes ( ) , & response )
2020-01-21 09:54:05 +00:00
if err != nil {
providerLog ( logger . LevelInfo , "interactive auth error parsing response: %v" , err )
once . Do ( func ( ) { terminateInteractiveAuthProgram ( cmd , false ) } )
break
}
if response . AuthResult != 0 {
authResult = response . AuthResult
break
}
2020-04-01 21:25:23 +00:00
if err = validateKeyboardAuthResponse ( response ) ; err != nil {
2020-01-21 09:54:05 +00:00
once . Do ( func ( ) { terminateInteractiveAuthProgram ( cmd , false ) } )
break
}
go func ( ) {
2020-08-19 17:36:12 +00:00
err := handleProgramInteractiveQuestions ( client , response , user , stdin , ip , protocol )
2020-01-21 09:54:05 +00:00
if err != nil {
once . Do ( func ( ) { terminateInteractiveAuthProgram ( cmd , false ) } )
}
} ( )
}
stdin . Close ( )
once . Do ( func ( ) { terminateInteractiveAuthProgram ( cmd , true ) } )
2020-04-30 12:23:55 +00:00
go func ( ) {
_ , err := cmd . Process . Wait ( )
if err != nil {
providerLog ( logger . LevelWarn , "error waiting for #%v process to exit: %v" , authHook , err )
}
} ( )
2020-04-01 21:25:23 +00:00
return authResult , err
}
2020-08-19 17:36:12 +00:00
func doKeyboardInteractiveAuth ( user User , authHook string , client ssh . KeyboardInteractiveChallenge , ip , protocol string ) ( User , error ) {
2020-04-01 21:25:23 +00:00
var authResult int
var err error
if strings . HasPrefix ( authHook , "http" ) {
2020-08-19 17:36:12 +00:00
authResult , err = executeKeyboardInteractiveHTTPHook ( user , authHook , client , ip , protocol )
2020-04-01 21:25:23 +00:00
} else {
2020-08-19 17:36:12 +00:00
authResult , err = executeKeyboardInteractiveProgram ( user , authHook , client , ip , protocol )
2020-04-01 21:25:23 +00:00
}
2020-04-03 20:30:30 +00:00
if err != nil {
return user , err
}
2020-01-21 09:54:05 +00:00
if authResult != 1 {
return user , fmt . Errorf ( "keyboard interactive auth failed, result: %v" , authResult )
}
2020-02-23 17:50:59 +00:00
err = checkLoginConditions ( user )
if err != nil {
return user , err
}
2020-01-21 09:54:05 +00:00
return user , nil
}
2020-08-19 17:36:12 +00:00
func isCheckPasswordHookDefined ( protocol string ) bool {
if len ( config . CheckPasswordHook ) == 0 {
return false
}
if config . CheckPasswordScope == 0 {
return true
}
switch protocol {
case "SSH" :
return config . CheckPasswordScope & 1 != 0
case "FTP" :
return config . CheckPasswordScope & 2 != 0
case "DAV" :
return config . CheckPasswordScope & 4 != 0
default :
return false
}
}
func getPasswordHookResponse ( username , password , ip , protocol string ) ( [ ] byte , error ) {
if strings . HasPrefix ( config . CheckPasswordHook , "http" ) {
var result [ ] byte
var url * url . URL
url , err := url . Parse ( config . CheckPasswordHook )
if err != nil {
providerLog ( logger . LevelWarn , "invalid url for check password hook %#v, error: %v" , config . CheckPasswordHook , err )
return result , err
}
req := checkPasswordRequest {
Username : username ,
Password : password ,
IP : ip ,
Protocol : protocol ,
}
reqAsJSON , err := json . Marshal ( req )
if err != nil {
return result , err
}
httpClient := httpclient . GetHTTPClient ( )
resp , err := httpClient . Post ( url . String ( ) , "application/json" , bytes . NewBuffer ( reqAsJSON ) )
if err != nil {
providerLog ( logger . LevelWarn , "error getting check password hook response: %v" , err )
return result , err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return result , fmt . Errorf ( "wrong http status code from chek password hook: %v, expected 200" , resp . StatusCode )
}
return ioutil . ReadAll ( resp . Body )
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
defer cancel ( )
cmd := exec . CommandContext ( ctx , config . CheckPasswordHook )
cmd . Env = append ( os . Environ ( ) ,
fmt . Sprintf ( "SFTPGO_AUTHD_USERNAME=%v" , username ) ,
fmt . Sprintf ( "SFTPGO_AUTHD_PASSWORD=%v" , password ) ,
fmt . Sprintf ( "SFTPGO_AUTHD_IP=%v" , ip ) ,
fmt . Sprintf ( "SFTPGO_AUTHD_PROTOCOL=%v" , protocol ) ,
)
return cmd . Output ( )
}
func executeCheckPasswordHook ( username , password , ip , protocol string ) ( checkPasswordResponse , error ) {
var response checkPasswordResponse
if ! isCheckPasswordHookDefined ( protocol ) {
response . Status = - 1
return response , nil
}
out , err := getPasswordHookResponse ( username , password , ip , protocol )
if err != nil {
return response , err
}
err = json . Unmarshal ( out , & response )
return response , err
}
2020-08-12 14:15:12 +00:00
func getPreLoginHookResponse ( loginMethod , ip , protocol string , userAsJSON [ ] byte ) ( [ ] byte , error ) {
2020-04-01 21:25:23 +00:00
if strings . HasPrefix ( config . PreLoginHook , "http" ) {
var url * url . URL
var result [ ] byte
url , err := url . Parse ( config . PreLoginHook )
if err != nil {
providerLog ( logger . LevelWarn , "invalid url for pre-login hook %#v, error: %v" , config . PreLoginHook , err )
return result , err
}
q := url . Query ( )
q . Add ( "login_method" , loginMethod )
2020-08-04 16:03:28 +00:00
q . Add ( "ip" , ip )
2020-08-12 14:15:12 +00:00
q . Add ( "protocol" , protocol )
2020-04-01 21:25:23 +00:00
url . RawQuery = q . Encode ( )
2020-04-26 21:29:09 +00:00
httpClient := httpclient . GetHTTPClient ( )
2020-04-01 21:25:23 +00:00
resp , err := httpClient . Post ( url . String ( ) , "application/json" , bytes . NewBuffer ( userAsJSON ) )
if err != nil {
providerLog ( logger . LevelWarn , "error getting pre-login hook response: %v" , err )
return result , err
}
defer resp . Body . Close ( )
if resp . StatusCode == http . StatusNoContent {
return result , nil
}
if resp . StatusCode != http . StatusOK {
return result , fmt . Errorf ( "wrong pre-login hook http status code: %v, expected 200" , resp . StatusCode )
}
return ioutil . ReadAll ( resp . Body )
}
2020-04-26 21:29:09 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
2020-04-01 21:25:23 +00:00
defer cancel ( )
cmd := exec . CommandContext ( ctx , config . PreLoginHook )
cmd . Env = append ( os . Environ ( ) ,
fmt . Sprintf ( "SFTPGO_LOGIND_USER=%v" , string ( userAsJSON ) ) ,
fmt . Sprintf ( "SFTPGO_LOGIND_METHOD=%v" , loginMethod ) ,
2020-08-04 16:03:28 +00:00
fmt . Sprintf ( "SFTPGO_LOGIND_IP=%v" , ip ) ,
2020-08-19 17:36:12 +00:00
fmt . Sprintf ( "SFTPGO_LOGIND_PROTOCOL=%v" , protocol ) ,
2020-04-01 21:25:23 +00:00
)
return cmd . Output ( )
}
2020-08-12 14:15:12 +00:00
func executePreLoginHook ( username , loginMethod , ip , protocol string ) ( User , error ) {
2020-02-23 17:50:59 +00:00
u , err := provider . userExists ( username )
if err != nil {
2020-03-27 22:26:22 +00:00
if _ , ok := err . ( * RecordNotFoundError ) ; ! ok {
return u , err
}
u = User {
ID : 0 ,
Username : username ,
}
2020-02-23 17:50:59 +00:00
}
userAsJSON , err := json . Marshal ( u )
if err != nil {
return u , err
}
2020-08-12 14:15:12 +00:00
out , err := getPreLoginHookResponse ( loginMethod , ip , protocol , userAsJSON )
2020-02-23 17:50:59 +00:00
if err != nil {
2020-04-01 21:25:23 +00:00
return u , fmt . Errorf ( "Pre-login hook error: %v" , err )
2020-02-23 17:50:59 +00:00
}
if len ( strings . TrimSpace ( string ( out ) ) ) == 0 {
2020-04-01 21:25:23 +00:00
providerLog ( logger . LevelDebug , "empty response from pre-login hook, no modification requested for user %#v id: %v" ,
2020-03-27 22:26:22 +00:00
username , u . ID )
if u . ID == 0 {
return u , & RecordNotFoundError { err : fmt . Sprintf ( "username %v does not exist" , username ) }
}
2020-02-23 17:50:59 +00:00
return u , nil
}
userID := u . ID
userUsedQuotaSize := u . UsedQuotaSize
userUsedQuotaFiles := u . UsedQuotaFiles
userLastQuotaUpdate := u . LastQuotaUpdate
userLastLogin := u . LastLogin
err = json . Unmarshal ( out , & u )
if err != nil {
2020-04-01 21:25:23 +00:00
return u , fmt . Errorf ( "Invalid pre-login hook response %#v, error: %v" , string ( out ) , err )
2020-02-23 17:50:59 +00:00
}
u . ID = userID
u . UsedQuotaSize = userUsedQuotaSize
u . UsedQuotaFiles = userUsedQuotaFiles
u . LastQuotaUpdate = userLastQuotaUpdate
u . LastLogin = userLastLogin
2020-03-27 22:26:22 +00:00
if userID == 0 {
err = provider . addUser ( u )
} else {
err = provider . updateUser ( u )
}
2020-02-23 17:50:59 +00:00
if err != nil {
return u , err
}
2020-04-01 21:25:23 +00:00
providerLog ( logger . LevelDebug , "user %#v added/updated from pre-login hook response, id: %v" , username , userID )
2020-02-23 17:50:59 +00:00
return provider . userExists ( username )
}
2020-08-12 14:15:12 +00:00
// ExecutePostLoginHook executes the post login hook if defined
func ExecutePostLoginHook ( username , loginMethod , ip , protocol string , err error ) {
if len ( config . PostLoginHook ) == 0 {
return
}
if config . PostLoginScope == 1 && err == nil {
return
}
if config . PostLoginScope == 2 && err != nil {
return
}
go func ( username , loginMethod , ip , protocol string , err error ) {
status := 0
if err == nil {
status = 1
}
if strings . HasPrefix ( config . PostLoginHook , "http" ) {
var url * url . URL
url , err := url . Parse ( config . PostLoginHook )
if err != nil {
providerLog ( logger . LevelDebug , "Invalid post-login hook %#v" , config . PostLoginHook )
return
}
postReq := make ( map [ string ] interface { } )
postReq [ "username" ] = username
postReq [ "login_method" ] = loginMethod
postReq [ "ip" ] = ip
postReq [ "protocol" ] = protocol
postReq [ "status" ] = status
postAsJSON , err := json . Marshal ( postReq )
if err != nil {
providerLog ( logger . LevelWarn , "error serializing post login request: %v" , err )
return
}
startTime := time . Now ( )
respCode := 0
httpClient := httpclient . GetHTTPClient ( )
resp , err := httpClient . Post ( url . String ( ) , "application/json" , bytes . NewBuffer ( postAsJSON ) )
if err == nil {
respCode = resp . StatusCode
resp . Body . Close ( )
}
providerLog ( logger . LevelDebug , "post login hook executed, response code: %v, elapsed: %v err: %v" ,
respCode , time . Since ( startTime ) , err )
return
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , 20 * time . Second )
defer cancel ( )
cmd := exec . CommandContext ( ctx , config . PostLoginHook )
cmd . Env = append ( os . Environ ( ) ,
fmt . Sprintf ( "SFTPGO_LOGIND_USER=%v" , username ) ,
fmt . Sprintf ( "SFTPGO_LOGIND_IP=%v" , ip ) ,
fmt . Sprintf ( "SFTPGO_LOGIND_METHOD=%v" , loginMethod ) ,
fmt . Sprintf ( "SFTPGO_LOGIND_STATUS=%v" , status ) ,
fmt . Sprintf ( "SFTPGO_LOGIND_PROTOCOL=%v" , protocol ) )
startTime := time . Now ( )
err = cmd . Run ( )
providerLog ( logger . LevelDebug , "post login hook executed, elapsed %v err: %v" , time . Since ( startTime ) , err )
} ( username , loginMethod , ip , protocol , err )
}
func getExternalAuthResponse ( username , password , pkey , keyboardInteractive , ip , protocol string ) ( [ ] byte , error ) {
2020-04-01 21:25:23 +00:00
if strings . HasPrefix ( config . ExternalAuthHook , "http" ) {
var url * url . URL
var result [ ] byte
url , err := url . Parse ( config . ExternalAuthHook )
if err != nil {
providerLog ( logger . LevelWarn , "invalid url for external auth hook %#v, error: %v" , config . ExternalAuthHook , err )
return result , err
}
2020-04-26 21:29:09 +00:00
httpClient := httpclient . GetHTTPClient ( )
2020-04-01 21:25:23 +00:00
authRequest := make ( map [ string ] string )
authRequest [ "username" ] = username
2020-08-04 16:03:28 +00:00
authRequest [ "ip" ] = ip
2020-04-01 21:25:23 +00:00
authRequest [ "password" ] = password
authRequest [ "public_key" ] = pkey
2020-08-12 14:15:12 +00:00
authRequest [ "protocol" ] = protocol
2020-04-01 21:25:23 +00:00
authRequest [ "keyboard_interactive" ] = keyboardInteractive
authRequestAsJSON , err := json . Marshal ( authRequest )
if err != nil {
providerLog ( logger . LevelWarn , "error serializing external auth request: %v" , err )
return result , err
}
resp , err := httpClient . Post ( url . String ( ) , "application/json" , bytes . NewBuffer ( authRequestAsJSON ) )
if err != nil {
providerLog ( logger . LevelWarn , "error getting external auth hook HTTP response: %v" , err )
return result , err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return result , fmt . Errorf ( "wrong external auth http status code: %v, expected 200" , resp . StatusCode )
}
return ioutil . ReadAll ( resp . Body )
}
2020-04-26 21:29:09 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
2020-04-01 21:25:23 +00:00
defer cancel ( )
cmd := exec . CommandContext ( ctx , config . ExternalAuthHook )
cmd . Env = append ( os . Environ ( ) ,
fmt . Sprintf ( "SFTPGO_AUTHD_USERNAME=%v" , username ) ,
2020-08-04 16:03:28 +00:00
fmt . Sprintf ( "SFTPGO_AUTHD_IP=%v" , ip ) ,
2020-04-01 21:25:23 +00:00
fmt . Sprintf ( "SFTPGO_AUTHD_PASSWORD=%v" , password ) ,
fmt . Sprintf ( "SFTPGO_AUTHD_PUBLIC_KEY=%v" , pkey ) ,
2020-08-12 14:15:12 +00:00
fmt . Sprintf ( "SFTPGO_AUTHD_PROTOCOL=%v" , protocol ) ,
2020-04-01 21:25:23 +00:00
fmt . Sprintf ( "SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v" , keyboardInteractive ) )
return cmd . Output ( )
}
2020-08-12 14:15:12 +00:00
func doExternalAuth ( username , password string , pubKey [ ] byte , keyboardInteractive , ip , protocol string ) ( User , error ) {
2020-01-06 20:42:41 +00:00
var user User
pkey := ""
if len ( pubKey ) > 0 {
2020-04-30 12:23:55 +00:00
k , err := ssh . ParsePublicKey ( pubKey )
2020-01-06 20:42:41 +00:00
if err != nil {
return user , err
}
pkey = string ( ssh . MarshalAuthorizedKey ( k ) )
}
2020-08-12 14:15:12 +00:00
out , err := getExternalAuthResponse ( username , password , pkey , keyboardInteractive , ip , protocol )
2020-01-06 20:42:41 +00:00
if err != nil {
2020-01-21 09:54:05 +00:00
return user , fmt . Errorf ( "External auth error: %v" , err )
2020-01-06 20:42:41 +00:00
}
err = json . Unmarshal ( out , & user )
if err != nil {
return user , fmt . Errorf ( "Invalid external auth response: %v" , err )
}
if len ( user . Username ) == 0 {
2020-08-31 17:25:17 +00:00
return user , ErrInvalidCredentials
2020-01-06 20:42:41 +00:00
}
2020-01-21 09:54:05 +00:00
if len ( password ) > 0 {
user . Password = password
}
2020-01-07 08:39:20 +00:00
if len ( pkey ) > 0 && ! utils . IsStringPrefixInSlice ( pkey , user . PublicKeys ) {
2020-01-06 20:42:41 +00:00
user . PublicKeys = append ( user . PublicKeys , pkey )
}
2020-06-08 11:06:02 +00:00
// some users want to map multiple login usernames with a single SGTPGo account
// for example an SFTP user logins using "user1" or "user2" and the external auth
// returns "user" in both cases, so we use the username returned from
// external auth and not the one used to login
u , err := provider . userExists ( user . Username )
2020-01-06 20:42:41 +00:00
if err == nil {
user . ID = u . ID
user . UsedQuotaSize = u . UsedQuotaSize
user . UsedQuotaFiles = u . UsedQuotaFiles
user . LastQuotaUpdate = u . LastQuotaUpdate
user . LastLogin = u . LastLogin
err = provider . updateUser ( user )
} else {
err = provider . addUser ( user )
}
if err != nil {
return user , err
}
2020-06-08 11:06:02 +00:00
return provider . userExists ( user . Username )
2020-01-06 20:42:41 +00:00
}
2019-09-06 13:19:01 +00:00
func providerLog ( level logger . LogLevel , format string , v ... interface { } ) {
logger . Log ( level , logSender , "" , format , v ... )
}
2019-11-14 10:06:03 +00:00
2020-02-05 21:17:03 +00:00
func executeNotificationCommand ( operation string , user User ) error {
2020-05-24 13:29:39 +00:00
if ! filepath . IsAbs ( config . Actions . Hook ) {
err := fmt . Errorf ( "invalid notification command %#v" , config . Actions . Hook )
logger . Warn ( logSender , "" , "unable to execute notification command: %v" , err )
return err
}
2020-01-09 11:00:37 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 15 * time . Second )
defer cancel ( )
commandArgs := user . getNotificationFieldsAsSlice ( operation )
2020-05-24 13:29:39 +00:00
cmd := exec . CommandContext ( ctx , config . Actions . Hook , commandArgs ... )
2020-01-09 11:00:37 +00:00
cmd . Env = append ( os . Environ ( ) , user . getNotificationFieldsAsEnvVars ( operation ) ... )
startTime := time . Now ( )
err := cmd . Run ( )
providerLog ( logger . LevelDebug , "executed command %#v with arguments: %+v, elapsed: %v, error: %v" ,
2020-05-24 13:29:39 +00:00
config . Actions . Hook , commandArgs , time . Since ( startTime ) , err )
2020-01-09 11:00:37 +00:00
return err
}
2019-11-14 10:06:03 +00:00
// executed in a goroutine
func executeAction ( operation string , user User ) {
if ! utils . IsStringInSlice ( operation , config . Actions . ExecuteOn ) {
return
}
2020-05-24 13:29:39 +00:00
if len ( config . Actions . Hook ) == 0 {
return
}
2019-11-14 10:06:03 +00:00
if operation != operationDelete {
var err error
user , err = provider . userExists ( user . Username )
if err != nil {
2020-02-05 21:17:03 +00:00
providerLog ( logger . LevelWarn , "unable to get the user to notify for operation %#v: %v" , operation , err )
2019-11-14 10:06:03 +00:00
return
}
}
2020-05-24 13:29:39 +00:00
if strings . HasPrefix ( config . Actions . Hook , "http" ) {
2019-11-14 10:06:03 +00:00
var url * url . URL
2020-05-24 13:29:39 +00:00
url , err := url . Parse ( config . Actions . Hook )
2019-11-14 10:06:03 +00:00
if err != nil {
2020-05-24 13:29:39 +00:00
providerLog ( logger . LevelWarn , "Invalid http_notification_url %#v for operation %#v: %v" , config . Actions . Hook , operation , err )
2019-11-14 10:06:03 +00:00
return
}
q := url . Query ( )
q . Add ( "action" , operation )
url . RawQuery = q . Encode ( )
2020-11-22 20:53:04 +00:00
user . HideConfidentialData ( )
2019-11-14 10:06:03 +00:00
userAsJSON , err := json . Marshal ( user )
if err != nil {
return
}
startTime := time . Now ( )
2020-04-26 21:29:09 +00:00
httpClient := httpclient . GetHTTPClient ( )
2019-11-14 10:06:03 +00:00
resp , err := httpClient . Post ( url . String ( ) , "application/json" , bytes . NewBuffer ( userAsJSON ) )
respCode := 0
if err == nil {
respCode = resp . StatusCode
resp . Body . Close ( )
}
providerLog ( logger . LevelDebug , "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v" ,
operation , url . String ( ) , respCode , time . Since ( startTime ) , err )
2020-05-24 13:29:39 +00:00
} else {
executeNotificationCommand ( operation , user ) //nolint:errcheck // the error is used in test cases only
2019-11-14 10:06:03 +00:00
}
}
2020-06-07 21:30:18 +00:00
// after migrating database to v4 we have to update the quota for the imported folders
func updateVFoldersQuotaAfterRestore ( foldersToScan [ ] string ) {
2020-11-02 18:16:12 +00:00
fs := vfs . NewOsFs ( "" , "" , nil ) . ( * vfs . OsFs )
2020-06-07 21:30:18 +00:00
for _ , folder := range foldersToScan {
providerLog ( logger . LevelDebug , "starting quota scan after migration for folder %#v" , folder )
vfolder , err := provider . getFolderByPath ( folder )
if err != nil {
providerLog ( logger . LevelWarn , "error getting folder to scan %#v: %v" , folder , err )
continue
}
numFiles , size , err := fs . GetDirSize ( folder )
if err != nil {
providerLog ( logger . LevelWarn , "error scanning folder %#v: %v" , folder , err )
continue
}
2020-07-08 17:59:31 +00:00
err = UpdateVirtualFolderQuota ( vfolder , numFiles , size , true )
2020-06-07 21:30:18 +00:00
providerLog ( logger . LevelDebug , "quota updated for virtual folder %#v, error: %v" , vfolder . MappedPath , err )
}
}
2020-08-31 17:25:17 +00:00
// CacheWebDAVUser add a user to the WebDAV cache
2020-11-04 18:11:40 +00:00
func CacheWebDAVUser ( cachedUser * CachedUser , maxSize int ) {
2020-08-31 17:25:17 +00:00
if maxSize > 0 {
var cacheSize int
var userToRemove string
var expirationTime time . Time
webDAVUsersCache . Range ( func ( k , v interface { } ) bool {
cacheSize ++
if len ( userToRemove ) == 0 {
userToRemove = k . ( string )
2020-11-04 18:11:40 +00:00
expirationTime = v . ( * CachedUser ) . Expiration
2020-08-31 17:25:17 +00:00
return true
}
2020-11-04 18:11:40 +00:00
expireTime := v . ( * CachedUser ) . Expiration
2020-08-31 17:25:17 +00:00
if ! expireTime . IsZero ( ) && expireTime . Before ( expirationTime ) {
userToRemove = k . ( string )
expirationTime = expireTime
}
return true
} )
if cacheSize >= maxSize {
RemoveCachedWebDAVUser ( userToRemove )
}
}
if len ( cachedUser . User . Username ) > 0 {
webDAVUsersCache . Store ( cachedUser . User . Username , cachedUser )
}
}
// GetCachedWebDAVUser returns a previously cached WebDAV user
func GetCachedWebDAVUser ( username string ) ( interface { } , bool ) {
return webDAVUsersCache . Load ( username )
}
// RemoveCachedWebDAVUser removes a cached WebDAV user
func RemoveCachedWebDAVUser ( username string ) {
if len ( username ) > 0 {
webDAVUsersCache . Delete ( username )
}
}