REST API v2

- add JWT authentication
- admins are now stored inside the data provider
- admin access can be restricted based on the source IP: both proxy
  header and connection IP are checked
- deprecate REST API CLI: it is not relevant anymore

Some other changes to the REST API can still happen before releasing
SFTPGo 2.0.0

Fixes #197
This commit is contained in:
Nicola Murino 2021-01-17 22:29:08 +01:00
parent d42fcc3786
commit 778ec9b88f
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
82 changed files with 9302 additions and 5327 deletions

View file

@ -98,13 +98,11 @@ jobs:
if: startsWith(matrix.os, 'windows-') != true
run: |
mkdir -p output/{bash_completion,zsh_completion}
mkdir -p output/examples/rest-api-cli
cp sftpgo output/
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
cp -r init output/
cp examples/rest-api-cli/sftpgo_api_cli output/examples/rest-api-cli/
./sftpgo gen completion bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1

View file

@ -94,12 +94,6 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
- name: Set up Python
if: startsWith(matrix.os, 'windows-')
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Build for Linux/macOS
if: startsWith(matrix.os, 'windows-') != true
run: go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -o sftpgo
@ -136,15 +130,6 @@ jobs:
env:
MATRIX_OS: ${{ matrix.os }}
- name: Build REST API CLI for Windows
if: startsWith(matrix.os, 'windows-')
run: |
python -m pip install --upgrade pip setuptools wheel
pip install requests
pip install pygments
pip install pyinstaller
pyinstaller --hidden-import="pkg_resources.py2_warn" --noupx --onefile examples\rest-api-cli\sftpgo_api_cli
- name: Gather cross build info
id: cross_info
if: ${{ matrix.os == 'ubuntu-latest' }}
@ -170,7 +155,7 @@ jobs:
- name: Prepare Release for Linux/macOS
if: startsWith(matrix.os, 'windows-') != true
run: |
mkdir -p output/{init,examples/rest-api-cli,sqlite,bash_completion,zsh_completion}
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
echo "For documentation please take a look here:" > output/README.txt
echo "" >> output/README.txt
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
@ -190,7 +175,6 @@ jobs:
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
cp examples/rest-api-cli/sftpgo_api_cli output/examples/rest-api-cli/
if [ $OS == 'linux' ]
then
cp -r output output_arm64
@ -254,7 +238,6 @@ jobs:
copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output
copy .\sftpgo.db .\output
copy .\dist\sftpgo_api_cli.exe .\output
copy .\LICENSE .\output\LICENSE.txt
mkdir output\templates
xcopy .\templates .\output\templates\ /E
@ -268,11 +251,10 @@ jobs:
- name: Prepare Portable Release for Windows
if: startsWith(matrix.os, 'windows-')
run: |
mkdir win-portable\examples\rest-api-cli
mkdir win-portable
copy .\sftpgo.exe .\win-portable
copy .\sftpgo.json .\win-portable
copy .\sftpgo.db .\win-portable
copy .\dist\sftpgo_api_cli.exe .\win-portable\examples\rest-api-cli
copy .\LICENSE .\win-portable\LICENSE.txt
mkdir win-portable\templates
xcopy .\templates .\win-portable\templates\ /E

View file

@ -46,7 +46,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy
- Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP/FTP/WebDAV service without losing the information about the client's address.
- [REST API](./docs/rest-api.md) for users and folders management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
- Easy [migration](./examples/rest-api-cli#convert-users-from-other-stores) from Linux system user accounts.
- Easy [migration](./examples/convertusers) from Linux system user accounts.
- [Portable mode](./docs/portable-mode.md): a convenient way to share a single directory on demand.
- [SFTP subsystem mode](./docs/sftp-subsystem.md): you can use SFTPGo as OpenSSH's SFTP subsystem.
- Performance analysis using built-in [profiler](./docs/profiling.md).
@ -147,7 +147,6 @@ After starting SFTPGo you can manage users and folders using:
- the [web based administration interface](./docs/web-admin.md)
- the [REST API](./docs/rest-api.md)
- the sample [REST API CLI](./examples/rest-api-cli)
To support embedded data providers like `bolt` and `SQLite` we can't have a CLI that directly write users and folders to the data provider, we always have to use the REST API.

View file

@ -84,7 +84,7 @@ Command-line flags should be specified in the Subsystem declaration.
dataProviderConf.PreferDatabaseCredentials = true
}
config.SetProviderConf(dataProviderConf)
err = dataprovider.Initialize(dataProviderConf, configDir)
err = dataprovider.Initialize(dataProviderConf, configDir, false)
if err != nil {
logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err)
os.Exit(1)

View file

@ -179,7 +179,7 @@ func initializeDataprovider(trackQuota int) (string, error) {
if trackQuota >= 0 && trackQuota <= 2 {
cfg.Config.TrackQuota = trackQuota
}
return cfg.Config.Driver, dataprovider.Initialize(cfg.Config, configDir)
return cfg.Config.Driver, dataprovider.Initialize(cfg.Config, configDir, true)
}
func closeDataprovider() error {

View file

@ -1057,10 +1057,10 @@ func TestHasSpace(t *testing.T) {
quotaResult = c.HasSpace(true, "/vdir/file1")
assert.False(t, quotaResult.HasSpace)
err = dataprovider.DeleteUser(&user)
err = dataprovider.DeleteUser(user.Username)
assert.NoError(t, err)
err = dataprovider.DeleteFolder(&folder)
err = dataprovider.DeleteFolder(folder.MappedPath)
assert.NoError(t, err)
}
@ -1133,7 +1133,7 @@ func TestUpdateQuotaMoveVFolders(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, folder2.UsedQuotaFiles)
assert.Equal(t, int64(100), folder2.UsedQuotaSize)
user, err = dataprovider.GetUserByID(user.ID)
user, err = dataprovider.UserExists(user.Username)
assert.NoError(t, err)
assert.Equal(t, 1, user.UsedQuotaFiles)
assert.Equal(t, int64(100), user.UsedQuotaSize)
@ -1143,16 +1143,16 @@ func TestUpdateQuotaMoveVFolders(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 2, folder2.UsedQuotaFiles)
assert.Equal(t, int64(200), folder2.UsedQuotaSize)
user, err = dataprovider.GetUserByID(user.ID)
user, err = dataprovider.UserExists(user.Username)
assert.NoError(t, err)
assert.Equal(t, 1, user.UsedQuotaFiles)
assert.Equal(t, int64(100), user.UsedQuotaSize)
err = dataprovider.DeleteUser(&user)
err = dataprovider.DeleteUser(user.Username)
assert.NoError(t, err)
err = dataprovider.DeleteFolder(&folder1)
err = dataprovider.DeleteFolder(folder1.MappedPath)
assert.NoError(t, err)
err = dataprovider.DeleteFolder(&folder2)
err = dataprovider.DeleteFolder(folder2.MappedPath)
assert.NoError(t, err)
}

View file

@ -293,8 +293,8 @@ func (t *BaseTransfer) HandleThrottle() {
if wantedBandwidth > 0 {
// real and wanted elapsed as milliseconds, bytes as kilobytes
realElapsed := time.Since(t.start).Nanoseconds() / 1000000
// trasferredBytes / 1000 = KB/s, we multiply for 1000 to get milliseconds
wantedElapsed := 1000 * (trasferredBytes / 1000) / wantedBandwidth
// trasferredBytes / 1024 = KB/s, we multiply for 1000 to get milliseconds
wantedElapsed := 1000 * (trasferredBytes / 1024) / wantedBandwidth
if wantedElapsed > realElapsed {
toSleep := time.Duration(wantedElapsed - realElapsed)
time.Sleep(toSleep * time.Millisecond)

View file

@ -57,8 +57,8 @@ func TestTransferThrottling(t *testing.T) {
}
fs := vfs.NewOsFs("", os.TempDir(), nil)
testFileSize := int64(131072)
wantedUploadElapsed := 1000 * (testFileSize / 1000) / u.UploadBandwidth
wantedDownloadElapsed := 1000 * (testFileSize / 1000) / u.DownloadBandwidth
wantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth
wantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth
// some tolerance
wantedUploadElapsed -= wantedDownloadElapsed / 10
wantedDownloadElapsed -= wantedDownloadElapsed / 10

View file

@ -175,7 +175,6 @@ func Init() {
Password: "",
ConnectionString: "",
SQLTablesPrefix: "",
ManageUsers: 1,
SSLMode: 0,
TrackQuota: 1,
PoolSize: 0,
@ -208,7 +207,6 @@ func Init() {
TemplatesPath: "templates",
StaticFilesPath: "static",
BackupsPath: "backups",
AuthUserFile: "",
CertificateFile: "",
CertificateKeyFile: "",
},
@ -749,7 +747,6 @@ func setViperDefaults() {
viper.SetDefault("data_provider.sslmode", globalConf.ProviderConf.SSLMode)
viper.SetDefault("data_provider.connection_string", globalConf.ProviderConf.ConnectionString)
viper.SetDefault("data_provider.sql_tables_prefix", globalConf.ProviderConf.SQLTablesPrefix)
viper.SetDefault("data_provider.manage_users", globalConf.ProviderConf.ManageUsers)
viper.SetDefault("data_provider.track_quota", globalConf.ProviderConf.TrackQuota)
viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize)
viper.SetDefault("data_provider.users_base_dir", globalConf.ProviderConf.UsersBaseDir)
@ -773,7 +770,6 @@ func setViperDefaults() {
viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath)
viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath)
viper.SetDefault("httpd.auth_user_file", globalConf.HTTPDConfig.AuthUserFile)
viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile)
viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile)
viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout)

228
dataprovider/admin.go Normal file
View file

@ -0,0 +1,228 @@
package dataprovider
import (
"encoding/base64"
"errors"
"fmt"
"net"
"regexp"
"strings"
"github.com/alexedwards/argon2id"
"github.com/minio/sha256-simd"
"github.com/drakkan/sftpgo/utils"
)
// Available permissions for SFTPGo admins
const (
PermAdminAny = "*"
PermAdminAddUsers = "add_users"
PermAdminChangeUsers = "edit_users"
PermAdminDeleteUsers = "del_users"
PermAdminViewUsers = "view_users"
PermAdminViewConnections = "view_conns"
PermAdminCloseConnections = "close_conns"
PermAdminViewServerStatus = "view_status"
PermAdminManageAdmins = "manage_admins"
PermAdminQuotaScans = "quota_scans"
PermAdminManageSystem = "manage_system"
PermAdminManageDefender = "manage_defender"
PermAdminViewDefender = "view_defender"
)
var (
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
PermAdminManageAdmins, PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender,
PermAdminViewDefender}
)
// AdminFilters defines additional restrictions for SFTPGo admins
type AdminFilters struct {
// only clients connecting from these IP/Mask are allowed.
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
// for example "192.0.2.0/24" or "2001:db8::/32"
AllowList []string `json:"allow_list,omitempty"`
}
// Admin defines a SFTPGo admin
type Admin struct {
// Database unique identifier
ID int64 `json:"id"`
// 1 enabled, 0 disabled (login is not allowed)
Status int `json:"status"`
// Username
Username string `json:"username"`
Password string `json:"password,omitempty"`
Email string `json:"email"`
Permissions []string `json:"permissions"`
Filters AdminFilters `json:"filters,omitempty"`
AdditionalInfo string `json:"additional_info,omitempty"`
}
func (a *Admin) validate() error {
if a.Username == "" {
return &ValidationError{err: "username is mandatory"}
}
if a.Password == "" {
return &ValidationError{err: "please set a password"}
}
if !usernameRegex.MatchString(a.Username) {
return &ValidationError{err: fmt.Sprintf("username %#v is not valid", a.Username)}
}
if a.Password != "" && !strings.HasPrefix(a.Password, argonPwdPrefix) {
pwd, err := argon2id.CreateHash(a.Password, argon2Params)
if err != nil {
return err
}
a.Password = pwd
}
a.Permissions = utils.RemoveDuplicates(a.Permissions)
if len(a.Permissions) == 0 {
return &ValidationError{err: "please grant some permissions to this admin"}
}
if utils.IsStringInSlice(PermAdminAny, a.Permissions) {
a.Permissions = []string{PermAdminAny}
}
for _, perm := range a.Permissions {
if !utils.IsStringInSlice(perm, validAdminPerms) {
return &ValidationError{err: fmt.Sprintf("invalid permission: %#v", perm)}
}
}
if a.Email != "" && !emailRegex.MatchString(a.Email) {
return &ValidationError{err: fmt.Sprintf("email %#v is not valid", a.Email)}
}
for _, IPMask := range a.Filters.AllowList {
_, _, err := net.ParseCIDR(IPMask)
if err != nil {
return &ValidationError{err: fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)}
}
}
return nil
}
// CheckPassword verifies the admin password
func (a *Admin) CheckPassword(password string) (bool, error) {
return argon2id.ComparePasswordAndHash(password, a.Password)
}
// CanLoginFromIP returns true if login from the given IP is allowed
func (a *Admin) CanLoginFromIP(ip string) bool {
if len(a.Filters.AllowList) == 0 {
return true
}
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return len(a.Filters.AllowList) == 0
}
for _, ipMask := range a.Filters.AllowList {
_, network, err := net.ParseCIDR(ipMask)
if err != nil {
continue
}
if network.Contains(parsedIP) {
return true
}
}
return false
}
func (a *Admin) checkUserAndPass(password, ip string) error {
if a.Status != 1 {
return fmt.Errorf("admin %#v is disabled", a.Username)
}
if a.Password == "" || password == "" {
return errors.New("credentials cannot be null or empty")
}
match, err := a.CheckPassword(password)
if err != nil {
return err
}
if !match {
return ErrInvalidCredentials
}
if !a.CanLoginFromIP(ip) {
return fmt.Errorf("login from IP %v not allowed", ip)
}
return nil
}
// HideConfidentialData hides admin confidential data
func (a *Admin) HideConfidentialData() {
a.Password = ""
}
// HasPermission returns true if the admin has the specified permission
func (a *Admin) HasPermission(perm string) bool {
if utils.IsStringInSlice(PermAdminAny, a.Permissions) {
return true
}
return utils.IsStringInSlice(perm, a.Permissions)
}
// GetPermissionsAsString returns permission as string
func (a *Admin) GetPermissionsAsString() string {
return strings.Join(a.Permissions, ", ")
}
// GetAllowedIPAsString returns the allowed IP as comma separated string
func (a *Admin) GetAllowedIPAsString() string {
return strings.Join(a.Filters.AllowList, ",")
}
// GetValidPerms returns the allowed admin permissions
func (a *Admin) GetValidPerms() []string {
return validAdminPerms
}
// GetInfoString returns admin's info as string.
func (a *Admin) GetInfoString() string {
var result string
if a.Email != "" {
result = fmt.Sprintf("Email: %v. ", a.Email)
}
if len(a.Filters.AllowList) > 0 {
result += fmt.Sprintf("Allowed IP/Mask: %v. ", len(a.Filters.AllowList))
}
return result
}
// GetSignature returns a signature for this admin.
// It could change after an update
func (a *Admin) GetSignature() string {
data := []byte(a.Username)
data = append(data, []byte(a.Password)...)
signature := sha256.Sum256(data)
return base64.StdEncoding.EncodeToString(signature[:])
}
func (a *Admin) getACopy() Admin {
permissions := make([]string, len(a.Permissions))
copy(permissions, a.Permissions)
filters := AdminFilters{}
filters.AllowList = make([]string, len(a.Filters.AllowList))
copy(filters.AllowList, a.Filters.AllowList)
return Admin{
ID: a.ID,
Status: a.Status,
Username: a.Username,
Password: a.Password,
Email: a.Email,
Permissions: permissions,
Filters: filters,
AdditionalInfo: a.AdditionalInfo,
}
}
// setDefaults sets the appropriate value for the default admin
func (a *Admin) setDefaults() {
a.Username = "admin"
a.Password = "password"
a.Status = 1
a.Permissions = []string{PermAdminAny}
}

View file

@ -3,7 +3,6 @@
package dataprovider
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
@ -23,11 +22,12 @@ const (
)
var (
usersBucket = []byte("users")
usersIDIdxBucket = []byte("users_id_idx")
foldersBucket = []byte("folders")
dbVersionBucket = []byte("db_version")
dbVersionKey = []byte("version")
usersBucket = []byte("users")
//usersIDIdxBucket = []byte("users_id_idx")
foldersBucket = []byte("folders")
adminsBucket = []byte("admins")
dbVersionBucket = []byte("db_version")
dbVersionKey = []byte("version")
)
// BoltProvider auth provider for bolt key/value store
@ -63,10 +63,6 @@ func initializeBoltProvider(basePath string) error {
providerLog(logger.LevelWarn, "error creating users bucket: %v", err)
return err
}
err = dbHandle.Update(func(tx *bolt.Tx) error {
_, e := tx.CreateBucketIfNotExists(usersIDIdxBucket)
return e
})
if err != nil {
providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err)
return err
@ -76,7 +72,15 @@ func initializeBoltProvider(basePath string) error {
return e
})
if err != nil {
providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err)
providerLog(logger.LevelWarn, "error creating folders bucket: %v", err)
return err
}
err = dbHandle.Update(func(tx *bolt.Tx) error {
_, e := tx.CreateBucketIfNotExists(adminsBucket)
return e
})
if err != nil {
providerLog(logger.LevelWarn, "error creating admins bucket: %v", err)
return err
}
err = dbHandle.Update(func(tx *bolt.Tx) error {
@ -87,19 +91,19 @@ func initializeBoltProvider(basePath string) error {
providerLog(logger.LevelWarn, "error creating database version bucket: %v", err)
return err
}
provider = BoltProvider{dbHandle: dbHandle}
provider = &BoltProvider{dbHandle: dbHandle}
} else {
providerLog(logger.LevelWarn, "error creating bolt key/value store handler: %v", err)
}
return err
}
func (p BoltProvider) checkAvailability() error {
func (p *BoltProvider) checkAvailability() error {
_, err := getBoltDatabaseVersion(p.dbHandle)
return err
}
func (p BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
func (p *BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
var user User
if password == "" {
return user, errors.New("Credentials cannot be null or empty")
@ -112,7 +116,17 @@ func (p BoltProvider) validateUserAndPass(username, password, ip, protocol strin
return checkUserAndPass(user, password, ip, protocol)
}
func (p BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
func (p *BoltProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
admin, err := p.adminExists(username)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
return admin, err
}
err = admin.checkUserAndPass(password, ip)
return admin, err
}
func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
var user User
if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty")
@ -125,36 +139,9 @@ func (p BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (Use
return checkUserAndPubKey(user, pubKey)
}
func (p BoltProvider) getUserByID(ID int64) (User, error) {
var user User
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, idxBucket, err := getBuckets(tx)
if err != nil {
return err
}
userIDAsBytes := itob(ID)
username := idxBucket.Get(userIDAsBytes)
if username == nil {
return &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)}
}
u := bucket.Get(username)
if u == nil {
return &RecordNotFoundError{err: fmt.Sprintf("username %#v and ID: %v does not exist", string(username), ID)}
}
folderBucket, err := getFolderBucket(tx)
if err != nil {
return err
}
user, err = joinUserAndFolders(u, folderBucket)
return err
})
return user, err
}
func (p BoltProvider) updateLastLogin(username string) error {
func (p *BoltProvider) updateLastLogin(username string) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -182,9 +169,9 @@ func (p BoltProvider) updateLastLogin(username string) error {
})
}
func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -216,7 +203,7 @@ func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64,
})
}
func (p BoltProvider) getUsedQuota(username string) (int, int64, error) {
func (p *BoltProvider) getUsedQuota(username string) (int, int64, error) {
user, err := p.userExists(username)
if err != nil {
providerLog(logger.LevelWarn, "unable to get quota for user %v error: %v", username, err)
@ -225,10 +212,173 @@ func (p BoltProvider) getUsedQuota(username string) (int, int64, error) {
return user.UsedQuotaFiles, user.UsedQuotaSize, err
}
func (p BoltProvider) userExists(username string) (User, error) {
func (p *BoltProvider) adminExists(username string) (Admin, error) {
var admin Admin
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := getAdminBucket(tx)
if err != nil {
return err
}
a := bucket.Get([]byte(username))
if a == nil {
return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", username)}
}
return json.Unmarshal(a, &admin)
})
return admin, err
}
func (p *BoltProvider) addAdmin(admin *Admin) error {
err := admin.validate()
if err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getAdminBucket(tx)
if err != nil {
return err
}
if a := bucket.Get([]byte(admin.Username)); a != nil {
return fmt.Errorf("admin %v already exists", admin.Username)
}
id, err := bucket.NextSequence()
if err != nil {
return err
}
admin.ID = int64(id)
buf, err := json.Marshal(admin)
if err != nil {
return err
}
return bucket.Put([]byte(admin.Username), buf)
})
}
func (p *BoltProvider) updateAdmin(admin *Admin) error {
err := admin.validate()
if err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getAdminBucket(tx)
if err != nil {
return err
}
var a []byte
if a = bucket.Get([]byte(admin.Username)); a == nil {
return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", admin.Username)}
}
var oldAdmin Admin
err = json.Unmarshal(a, &oldAdmin)
if err != nil {
return err
}
admin.ID = oldAdmin.ID
buf, err := json.Marshal(admin)
if err != nil {
return err
}
return bucket.Put([]byte(admin.Username), buf)
})
}
func (p *BoltProvider) deleteAdmin(admin *Admin) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getAdminBucket(tx)
if err != nil {
return err
}
if bucket.Get([]byte(admin.Username)) == nil {
return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", admin.Username)}
}
return bucket.Delete([]byte(admin.Username))
})
}
func (p *BoltProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
admins := make([]Admin, 0, limit)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := getAdminBucket(tx)
if err != nil {
return err
}
cursor := bucket.Cursor()
itNum := 0
if order == OrderASC {
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
itNum++
if itNum <= offset {
continue
}
var admin Admin
err = json.Unmarshal(v, &admin)
if err != nil {
return err
}
admin.HideConfidentialData()
admins = append(admins, admin)
if len(admins) >= limit {
break
}
}
} else {
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
itNum++
if itNum <= offset {
continue
}
var admin Admin
err = json.Unmarshal(v, &admin)
if err != nil {
return err
}
admin.HideConfidentialData()
admins = append(admins, admin)
if len(admins) >= limit {
break
}
}
}
return err
})
return admins, err
}
func (p *BoltProvider) dumpAdmins() ([]Admin, error) {
admins := make([]Admin, 0, 30)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := getAdminBucket(tx)
if err != nil {
return err
}
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var admin Admin
err = json.Unmarshal(v, &admin)
if err != nil {
return err
}
admins = append(admins, admin)
}
return err
})
return admins, err
}
func (p *BoltProvider) userExists(username string) (User, error) {
var user User
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -246,13 +396,13 @@ func (p BoltProvider) userExists(username string) (User, error) {
return user, err
}
func (p BoltProvider) addUser(user *User) error {
func (p *BoltProvider) addUser(user *User) error {
err := validateUser(user)
if err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, idxBucket, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -282,22 +432,17 @@ func (p BoltProvider) addUser(user *User) error {
if err != nil {
return err
}
err = bucket.Put([]byte(user.Username), buf)
if err != nil {
return err
}
userIDAsBytes := itob(user.ID)
return idxBucket.Put(userIDAsBytes, []byte(user.Username))
return bucket.Put([]byte(user.Username), buf)
})
}
func (p BoltProvider) updateUser(user *User) error {
func (p *BoltProvider) updateUser(user *User) error {
err := validateUser(user)
if err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -339,9 +484,9 @@ func (p BoltProvider) updateUser(user *User) error {
})
}
func (p BoltProvider) deleteUser(user *User) error {
func (p *BoltProvider) deleteUser(user *User) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, idxBucket, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -357,23 +502,18 @@ func (p BoltProvider) deleteUser(user *User) error {
}
}
}
userIDAsBytes := itob(user.ID)
userName := idxBucket.Get(userIDAsBytes)
if userName == nil {
return &RecordNotFoundError{err: fmt.Sprintf("user with id %v does not exist", user.ID)}
exists := bucket.Get([]byte(user.Username))
if exists == nil {
return &RecordNotFoundError{err: fmt.Sprintf("user %#v does not exist", user.Username)}
}
err = bucket.Delete(userName)
if err != nil {
return err
}
return idxBucket.Delete(userIDAsBytes)
return bucket.Delete([]byte(user.Username))
})
}
func (p BoltProvider) dumpUsers() ([]User, error) {
func (p *BoltProvider) dumpUsers() ([]User, error) {
users := make([]User, 0, 100)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -398,35 +538,14 @@ func (p BoltProvider) dumpUsers() ([]User, error) {
return users, err
}
func (p BoltProvider) getUserWithUsername(username string) ([]User, error) {
users := []User{}
var user User
user, err := p.userExists(username)
if err == nil {
user.HideConfidentialData()
users = append(users, user)
return users, nil
}
if _, ok := err.(*RecordNotFoundError); ok {
err = nil
}
return users, err
}
func (p BoltProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
func (p *BoltProvider) getUsers(limit int, offset int, order string) ([]User, error) {
users := make([]User, 0, limit)
var err error
if limit <= 0 {
return users, err
}
if len(username) > 0 {
if offset == 0 {
return p.getUserWithUsername(username)
}
return users, err
}
err = p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -472,7 +591,7 @@ func (p BoltProvider) getUsers(limit int, offset int, order string, username str
return users, err
}
func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
func (p *BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
folders := make([]vfs.BaseVirtualFolder, 0, 50)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx)
@ -493,7 +612,7 @@ func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return folders, err
}
func (p BoltProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
func (p *BoltProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
folders := make([]vfs.BaseVirtualFolder, 0, limit)
var err error
if limit <= 0 {
@ -554,7 +673,7 @@ func (p BoltProvider) getFolders(limit, offset int, order, folderPath string) ([
return folders, err
}
func (p BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error) {
func (p *BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error) {
var folder vfs.BaseVirtualFolder
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx)
@ -567,7 +686,7 @@ func (p BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error
return folder, err
}
func (p BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
err := validateFolder(folder)
if err != nil {
return err
@ -585,13 +704,13 @@ func (p BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
})
}
func (p BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
func (p *BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx)
if err != nil {
return err
}
usersBucket, _, err := getBuckets(tx)
usersBucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -635,7 +754,7 @@ func (p BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
})
}
func (p BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx)
if err != nil {
@ -666,7 +785,7 @@ func (p BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd
})
}
func (p BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
func (p *BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
folder, err := p.getFolderByPath(mappedPath)
if err != nil {
providerLog(logger.LevelWarn, "unable to get quota for folder %#v error: %v", mappedPath, err)
@ -675,20 +794,20 @@ func (p BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error)
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
}
func (p BoltProvider) close() error {
func (p *BoltProvider) close() error {
return p.dbHandle.Close()
}
func (p BoltProvider) reloadConfig() error {
func (p *BoltProvider) reloadConfig() error {
return nil
}
// initializeDatabase does nothing, no initilization is needed for bolt provider
func (p BoltProvider) initializeDatabase() error {
func (p *BoltProvider) initializeDatabase() error {
return ErrNoInitRequired
}
func (p BoltProvider) migrateDatabase() error {
func (p *BoltProvider) migrateDatabase() error {
dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
if err != nil {
return err
@ -718,7 +837,7 @@ func (p BoltProvider) migrateDatabase() error {
}
}
func (p BoltProvider) revertDatabase(targetVersion int) error {
func (p *BoltProvider) revertDatabase(targetVersion int) error {
dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
if err != nil {
return err
@ -762,16 +881,12 @@ func updateBoltDatabaseFromV4(dbHandle *bolt.DB) error {
return updateDatabaseFrom4To5(dbHandle)
}
// itob returns an 8-byte big endian representation of v.
func itob(v int64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) {
var user User
err := json.Unmarshal(u, &user)
if err != nil {
return user, err
}
if len(user.VirtualFolders) > 0 {
var folders []vfs.VirtualFolder
for _, folder := range user.VirtualFolders {
@ -872,7 +987,7 @@ func removeUserFromFolderMapping(folder vfs.VirtualFolder, user *User, bucket *b
func updateV4BoltCompatUser(dbHandle *bolt.DB, user compatUserV4) error {
return dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -893,7 +1008,7 @@ func updateV4BoltUser(dbHandle *bolt.DB, user User) error {
return err
}
return dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -908,14 +1023,23 @@ func updateV4BoltUser(dbHandle *bolt.DB, user User) error {
})
}
func getBuckets(tx *bolt.Tx) (*bolt.Bucket, *bolt.Bucket, error) {
func getAdminBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
var err error
bucket := tx.Bucket(adminsBucket)
if bucket == nil {
err = errors.New("unable to find admin bucket, bolt database structure not correcly defined")
}
return bucket, err
}
func getUsersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
var err error
bucket := tx.Bucket(usersBucket)
idxBucket := tx.Bucket(usersIDIdxBucket)
if bucket == nil || idxBucket == nil {
err = fmt.Errorf("unable to find required buckets, bolt database structure not correcly defined")
if bucket == nil {
err = errors.New("unable to find required buckets, bolt database structure not correcly defined")
}
return bucket, idxBucket, err
return bucket, err
}
func getFolderBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
@ -954,7 +1078,7 @@ func updateDatabaseFrom2To3(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "updating bolt database version: 2 -> 3")
users := []User{}
err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -1011,7 +1135,7 @@ func updateDatabaseFrom3To4(dbHandle *bolt.DB) error {
foldersToScan := []string{}
users := []userCompactVFolders{}
err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -1075,7 +1199,7 @@ func downgradeBoltDatabaseFrom5To4(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "downgrading bolt database version: 5 -> 4")
users := []compatUserV4{}
err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -1116,7 +1240,7 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "updating bolt database version: 4 -> 5")
users := []User{}
err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
@ -1154,13 +1278,13 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error {
func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) {
usernames := []string{}
err := dbHandle.View(func(tx *bolt.Tx) error {
_, idxBucket, err := getBuckets(tx)
bucket, err := getUsersBucket(tx)
if err != nil {
return err
}
cursor := idxBucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
usernames = append(usernames, string(v))
cursor := bucket.Cursor()
for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() {
usernames = append(usernames, string(k))
}
return nil
})

View file

@ -24,6 +24,7 @@ import (
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
@ -62,7 +63,7 @@ const (
MemoryDataProviderName = "memory"
// DumpVersion defines the version for the dump.
// For restore/load we support the current version and the previous one
DumpVersion = 5
DumpVersion = 6
argonPwdPrefix = "$argon2id$"
bcryptPwdPrefix = "$2a$"
@ -73,7 +74,6 @@ const (
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"
@ -123,9 +123,11 @@ var (
sqlTableUsers = "users"
sqlTableFolders = "folders"
sqlTableFoldersMapping = "folders_mapping"
sqlTableAdmins = "admins"
sqlTableSchemaVersion = "schema_version"
argon2Params *argon2id.Params
lastLoginMinDelay = 10 * time.Minute
usernameRegex = regexp.MustCompile("^[a-zA-Z0-9-_.~]+$")
)
type schemaVersion struct {
@ -185,8 +187,6 @@ type Config struct {
ConnectionString string `json:"connection_string" mapstructure:"connection_string"`
// prefix for SQL tables
SQLTablesPrefix string `json:"sql_tables_prefix" mapstructure:"sql_tables_prefix"`
// Set to 0 to disable users management, 1 to enable
ManageUsers int `json:"manage_users" mapstructure:"manage_users"`
// 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
@ -277,6 +277,7 @@ type Config struct {
type BackupData struct {
Users []User `json:"users"`
Folders []vfs.BaseVirtualFolder `json:"folders"`
Admins []Admin `json:"admins"`
Version int `json:"version"`
}
@ -334,6 +335,13 @@ func (e *ValidationError) Error() string {
return fmt.Sprintf("Validation error: %s", e.err)
}
// NewValidationError returns a validation errors
func NewValidationError(error string) *ValidationError {
return &ValidationError{
err: error,
}
}
// MethodDisabledError raised if a method is disabled in config file.
// For example, if user management is disabled, this error is raised
// every time a user operation is done using the REST API
@ -370,9 +378,8 @@ type Provider interface {
addUser(user *User) error
updateUser(user *User) error
deleteUser(user *User) error
getUsers(limit int, offset int, order string, username string) ([]User, error)
getUsers(limit int, offset int, order string) ([]User, error)
dumpUsers() ([]User, error)
getUserByID(ID int64) (User, error)
updateLastLogin(username string) error
getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error)
getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error)
@ -381,6 +388,13 @@ type Provider interface {
updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error
getUsedFolderQuota(mappedPath string) (int, int64, error)
dumpFolders() ([]vfs.BaseVirtualFolder, error)
adminExists(username string) (Admin, error)
addAdmin(admin *Admin) error
updateAdmin(admin *Admin) error
deleteAdmin(admin *Admin) error
getAdmins(limit int, offset int, order string) ([]Admin, error)
dumpAdmins() ([]Admin, error)
validateAdminAndPass(username, password, ip string) (Admin, error)
checkAvailability() error
close() error
reloadConfig() error
@ -391,7 +405,7 @@ type Provider interface {
// Initialize the data provider.
// An error is returned if the configured driver is invalid or if the data provider cannot be initialized
func Initialize(cnf Config, basePath string) error {
func Initialize(cnf Config, basePath string, checkAdmins bool) error {
var err error
config = cnf
@ -408,6 +422,13 @@ func Initialize(cnf Config, basePath string) error {
if err != nil {
return err
}
argon2Params = &argon2id.Params{
Memory: cnf.PasswordHashing.Argon2Options.Memory,
Iterations: cnf.PasswordHashing.Argon2Options.Iterations,
Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism,
SaltLength: 16,
KeyLength: 32,
}
if cnf.UpdateMode == 0 {
err = provider.initializeDatabase()
if err != nil && err != ErrNoInitRequired {
@ -423,16 +444,16 @@ func Initialize(cnf Config, basePath string) error {
providerLog(logger.LevelWarn, "database migration error: %v", err)
return err
}
if checkAdmins {
err = checkDefaultAdmin()
if err != nil {
providerLog(logger.LevelWarn, "check default admin error: %v", err)
return err
}
}
} else {
providerLog(logger.LevelInfo, "database initialization/migration skipped, manual mode is configured")
}
argon2Params = &argon2id.Params{
Memory: cnf.PasswordHashing.Argon2Options.Memory,
Iterations: cnf.PasswordHashing.Argon2Options.Iterations,
Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism,
SaltLength: 16,
KeyLength: 32,
}
startAvailabilityTimer()
return nil
}
@ -476,13 +497,29 @@ func validateSQLTablesPrefix() error {
sqlTableUsers = config.SQLTablesPrefix + sqlTableUsers
sqlTableFolders = config.SQLTablesPrefix + sqlTableFolders
sqlTableFoldersMapping = config.SQLTablesPrefix + sqlTableFoldersMapping
sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v schema version %#v",
sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableSchemaVersion)
providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v admins %#v schema version %#v",
sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableAdmins, sqlTableSchemaVersion)
}
return nil
}
func checkDefaultAdmin() error {
admins, err := provider.getAdmins(1, 0, OrderASC)
if err != nil {
return err
}
if len(admins) > 0 {
return nil
}
logger.Debug(logSender, "", "no admins found, try to create the default one")
// we need to create the default admin
admin := &Admin{}
admin.setDefaults()
return provider.addAdmin(admin)
}
// InitializeDatabase creates the initial database structure
func InitializeDatabase(cnf Config, basePath string) error {
config = cnf
@ -525,6 +562,11 @@ func RevertDatabase(cnf Config, basePath string, targetVersion int) error {
return provider.revertDatabase(targetVersion)
}
// CheckAdminAndPass validates the given admin and password connecting from ip
func CheckAdminAndPass(username, password, ip string) (Admin, error) {
return provider.validateAdminAndPass(username, password, ip)
}
// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
@ -583,9 +625,6 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard
// UpdateLastLogin updates the last login fields for the given SFTP user
func UpdateLastLogin(user User) error {
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
}
lastLogin := utils.GetTimeFromMsecSinceEpoch(user.LastLogin)
diff := -time.Until(lastLogin)
if diff < 0 || diff > lastLoginMinDelay {
@ -606,9 +645,6 @@ func UpdateUserQuota(user User, filesAdd int, sizeAdd int64, reset bool) error {
} else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() {
return nil
}
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
}
if filesAdd == 0 && sizeAdd == 0 && !reset {
return nil
}
@ -621,9 +657,6 @@ func UpdateVirtualFolderQuota(vfolder vfs.BaseVirtualFolder, filesAdd int, sizeA
if config.TrackQuota == 0 {
return &MethodDisabledError{err: trackQuotaDisabledError}
}
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
}
if filesAdd == 0 && sizeAdd == 0 && !reset {
return nil
}
@ -646,17 +679,37 @@ func GetUsedVirtualFolderQuota(mappedPath string) (int, int64, error) {
return provider.getUsedFolderQuota(mappedPath)
}
// UserExists checks if the given SFTP username exists, returns an error if no match is found
// AddAdmin adds a new SFTPGo admin
func AddAdmin(admin *Admin) error {
return provider.addAdmin(admin)
}
// UpdateAdmin updates an existing SFTPGo admin
func UpdateAdmin(admin *Admin) error {
return provider.updateAdmin(admin)
}
// DeleteAdmin deletes an existing SFTPGo admin
func DeleteAdmin(username string) error {
admin, err := provider.adminExists(username)
if err != nil {
return err
}
return provider.deleteAdmin(&admin)
}
// AdminExists returns the given admins if it exists
func AdminExists(username string) (Admin, error) {
return provider.adminExists(username)
}
// UserExists checks if the given SFTPGo username exists, returns an error if no match is found
func UserExists(username string) (User, error) {
return provider.userExists(username)
}
// AddUser adds a new SFTPGo user.
// ManageUsers configuration must be set to 1 to enable this method
func AddUser(user *User) error {
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
}
err := provider.addUser(user)
if err == nil {
go executeAction(operationAdd, *user)
@ -665,11 +718,7 @@ func AddUser(user *User) error {
}
// UpdateUser updates an existing SFTPGo user.
// ManageUsers configuration must be set to 1 to enable this method
func UpdateUser(user *User) error {
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
}
err := provider.updateUser(user)
if err == nil {
RemoveCachedWebDAVUser(user.Username)
@ -679,15 +728,15 @@ func UpdateUser(user *User) error {
}
// DeleteUser deletes an existing SFTPGo user.
// ManageUsers configuration must be set to 1 to enable this method
func DeleteUser(user *User) error {
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
func DeleteUser(username string) error {
user, err := provider.userExists(username)
if err != nil {
return err
}
err := provider.deleteUser(user)
err = provider.deleteUser(&user)
if err == nil {
RemoveCachedWebDAVUser(user.Username)
go executeAction(operationDelete, *user)
go executeAction(operationDelete, user)
}
return err
}
@ -699,32 +748,28 @@ func ReloadConfig() error {
return provider.reloadConfig()
}
// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
func GetUsers(limit, offset int, order string, username string) ([]User, error) {
return provider.getUsers(limit, offset, order, username)
// GetAdmins returns an array of admins respecting limit and offset
func GetAdmins(limit, offset int, order string) ([]Admin, error) {
return provider.getAdmins(limit, offset, order)
}
// GetUserByID returns the user with the given database ID if a match is found or an error
func GetUserByID(ID int64) (User, error) {
return provider.getUserByID(ID)
// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
func GetUsers(limit, offset int, order string) ([]User, error) {
return provider.getUsers(limit, offset, order)
}
// AddFolder adds a new virtual folder.
// ManageUsers configuration must be set to 1 to enable this method
func AddFolder(folder *vfs.BaseVirtualFolder) error {
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
}
return provider.addFolder(folder)
}
// DeleteFolder deletes an existing folder.
// ManageUsers configuration must be set to 1 to enable this method
func DeleteFolder(folder *vfs.BaseVirtualFolder) error {
if config.ManageUsers == 0 {
return &MethodDisabledError{err: manageUsersDisabledError}
func DeleteFolder(folderPath string) error {
folder, err := provider.getFolderByPath(folderPath)
if err != nil {
return err
}
return provider.deleteFolder(folder)
return provider.deleteFolder(&folder)
}
// GetFolderByPath returns the folder with the specified path if any
@ -740,7 +785,6 @@ func GetFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualF
// DumpData returns all users and folders
func DumpData() (BackupData, error) {
var data BackupData
data.Version = DumpVersion
users, err := provider.dumpUsers()
if err != nil {
return data, err
@ -749,8 +793,14 @@ func DumpData() (BackupData, error) {
if err != nil {
return data, err
}
admins, err := provider.dumpAdmins()
if err != nil {
return data, err
}
data.Users = users
data.Folders = folders
data.Admins = admins
data.Version = DumpVersion
return data, err
}
@ -974,7 +1024,7 @@ func validatePermissions(user *User) error {
if utils.IsStringInSlice(PermAny, perms) {
permissions[cleanedDir] = []string{PermAny}
} else {
permissions[cleanedDir] = perms
permissions[cleanedDir] = utils.RemoveDuplicates(perms)
}
}
user.Permissions = permissions
@ -1238,6 +1288,9 @@ func validateBaseParams(user *User) error {
if user.Username == "" {
return &ValidationError{err: "username is mandatory"}
}
if !usernameRegex.MatchString(user.Username) {
return &ValidationError{err: fmt.Sprintf("username %#v is not valid", user.Username)}
}
if user.HomeDir == "" {
return &ValidationError{err: "home_dir is mandatory"}
}
@ -1251,7 +1304,7 @@ func validateBaseParams(user *User) error {
}
func createUserPasswordHash(user *User) error {
if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
if user.Password != "" && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
pwd, err := argon2id.CreateHash(user.Password, argon2Params)
if err != nil {
return err

View file

@ -26,14 +26,16 @@ type memoryProviderHandle struct {
isClosed bool
// slice with ordered usernames
usernames []string
// mapping between ID and username
usersIdx map[int64]string
// map for users, username is the key
users map[string]User
// map for virtual folders, MappedPath is the key
vfolders map[string]vfs.BaseVirtualFolder
// slice with ordered folders mapped path
vfoldersPaths []string
// map for admins, username is the key
admins map[string]Admin
// slice with ordered admins
adminsUsernames []string
}
// MemoryProvider auth provider for a memory store
@ -50,15 +52,16 @@ func initializeMemoryProvider(basePath string) {
configFile = filepath.Join(basePath, configFile)
}
}
provider = MemoryProvider{
provider = &MemoryProvider{
dbHandle: &memoryProviderHandle{
isClosed: false,
usernames: []string{},
usersIdx: make(map[int64]string),
users: make(map[string]User),
vfolders: make(map[string]vfs.BaseVirtualFolder),
vfoldersPaths: []string{},
configFile: configFile,
isClosed: false,
usernames: []string{},
users: make(map[string]User),
vfolders: make(map[string]vfs.BaseVirtualFolder),
vfoldersPaths: []string{},
admins: make(map[string]Admin),
adminsUsernames: []string{},
configFile: configFile,
},
}
if err := provider.reloadConfig(); err != nil {
@ -67,7 +70,7 @@ func initializeMemoryProvider(basePath string) {
}
}
func (p MemoryProvider) checkAvailability() error {
func (p *MemoryProvider) checkAvailability() error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -76,7 +79,7 @@ func (p MemoryProvider) checkAvailability() error {
return nil
}
func (p MemoryProvider) close() error {
func (p *MemoryProvider) close() error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -86,7 +89,7 @@ func (p MemoryProvider) close() error {
return nil
}
func (p MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
var user User
if password == "" {
return user, errors.New("Credentials cannot be null or empty")
@ -99,7 +102,7 @@ func (p MemoryProvider) validateUserAndPass(username, password, ip, protocol str
return checkUserAndPass(user, password, ip, protocol)
}
func (p MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
var user User
if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty")
@ -112,19 +115,17 @@ func (p MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (U
return checkUserAndPubKey(user, pubKey)
}
func (p MemoryProvider) getUserByID(ID int64) (User, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return User{}, errMemoryProviderClosed
func (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
admin, err := p.adminExists(username)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
return admin, err
}
if val, ok := p.dbHandle.usersIdx[ID]; ok {
return p.userExistsInternal(val)
}
return User{}, &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)}
err = admin.checkUserAndPass(password, ip)
return admin, err
}
func (p MemoryProvider) updateLastLogin(username string) error {
func (p *MemoryProvider) updateLastLogin(username string) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -139,7 +140,7 @@ func (p MemoryProvider) updateLastLogin(username string) error {
return nil
}
func (p MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -164,7 +165,7 @@ func (p MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64
return nil
}
func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) {
func (p *MemoryProvider) getUsedQuota(username string) (int, int64, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -178,7 +179,7 @@ func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) {
return user.UsedQuotaFiles, user.UsedQuotaSize, err
}
func (p MemoryProvider) addUser(user *User) error {
func (p *MemoryProvider) addUser(user *User) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -199,13 +200,12 @@ func (p MemoryProvider) addUser(user *User) error {
user.LastLogin = 0
user.VirtualFolders = p.joinVirtualFoldersFields(user)
p.dbHandle.users[user.Username] = user.getACopy()
p.dbHandle.usersIdx[user.ID] = user.Username
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
sort.Strings(p.dbHandle.usernames)
return nil
}
func (p MemoryProvider) updateUser(user *User) error {
func (p *MemoryProvider) updateUser(user *User) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -227,12 +227,13 @@ func (p MemoryProvider) updateUser(user *User) error {
user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles
user.LastLogin = u.LastLogin
user.ID = u.ID
// pre-login and external auth hook will use the passed *user so save a copy
p.dbHandle.users[user.Username] = user.getACopy()
return nil
}
func (p MemoryProvider) deleteUser(user *User) error {
func (p *MemoryProvider) deleteUser(user *User) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -246,7 +247,6 @@ func (p MemoryProvider) deleteUser(user *User) error {
p.removeUserFromFolderMapping(oldFolder.MappedPath, u.Username)
}
delete(p.dbHandle.users, user.Username)
delete(p.dbHandle.usersIdx, user.ID)
// this could be more efficient
p.dbHandle.usernames = make([]string, 0, len(p.dbHandle.users))
for username := range p.dbHandle.users {
@ -256,7 +256,7 @@ func (p MemoryProvider) deleteUser(user *User) error {
return nil
}
func (p MemoryProvider) dumpUsers() ([]User, error) {
func (p *MemoryProvider) dumpUsers() ([]User, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
users := make([]User, 0, len(p.dbHandle.usernames))
@ -276,7 +276,7 @@ func (p MemoryProvider) dumpUsers() ([]User, error) {
return users, err
}
func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
func (p *MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
folders := make([]vfs.BaseVirtualFolder, 0, len(p.dbHandle.vfoldersPaths))
@ -289,7 +289,7 @@ func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return folders, nil
}
func (p MemoryProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User, error) {
users := make([]User, 0, limit)
var err error
p.dbHandle.Lock()
@ -300,16 +300,6 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s
if limit <= 0 {
return users, err
}
if len(username) > 0 {
if offset == 0 {
user, err := p.userExistsInternal(username)
if err == nil {
user.HideConfidentialData()
users = append(users, user)
}
}
return users, err
}
itNum := 0
if order == OrderASC {
for _, username := range p.dbHandle.usernames {
@ -344,7 +334,7 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s
return users, err
}
func (p MemoryProvider) userExists(username string) (User, error) {
func (p *MemoryProvider) userExists(username string) (User, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -353,14 +343,152 @@ func (p MemoryProvider) userExists(username string) (User, error) {
return p.userExistsInternal(username)
}
func (p MemoryProvider) userExistsInternal(username string) (User, error) {
func (p *MemoryProvider) userExistsInternal(username string) (User, error) {
if val, ok := p.dbHandle.users[username]; ok {
return val.getACopy(), nil
}
return User{}, &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)}
}
func (p MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *MemoryProvider) addAdmin(admin *Admin) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
err := admin.validate()
if err != nil {
return err
}
_, err = p.adminExistsInternal(admin.Username)
if err == nil {
return fmt.Errorf("admin %#v already exists", admin.Username)
}
admin.ID = p.getNextAdminID()
p.dbHandle.admins[admin.Username] = admin.getACopy()
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username)
sort.Strings(p.dbHandle.adminsUsernames)
return nil
}
func (p *MemoryProvider) updateAdmin(admin *Admin) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
err := admin.validate()
if err != nil {
return err
}
a, err := p.adminExistsInternal(admin.Username)
if err != nil {
return err
}
admin.ID = a.ID
p.dbHandle.admins[admin.Username] = admin.getACopy()
return nil
}
func (p *MemoryProvider) deleteAdmin(admin *Admin) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err := p.adminExistsInternal(admin.Username)
if err != nil {
return err
}
delete(p.dbHandle.admins, admin.Username)
// this could be more efficient
p.dbHandle.adminsUsernames = make([]string, 0, len(p.dbHandle.admins))
for username := range p.dbHandle.admins {
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, username)
}
sort.Strings(p.dbHandle.adminsUsernames)
return nil
}
func (p *MemoryProvider) adminExists(username string) (Admin, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return Admin{}, errMemoryProviderClosed
}
return p.adminExistsInternal(username)
}
func (p *MemoryProvider) adminExistsInternal(username string) (Admin, error) {
if val, ok := p.dbHandle.admins[username]; ok {
return val.getACopy(), nil
}
return Admin{}, &RecordNotFoundError{err: fmt.Sprintf("admin %#v does not exist", username)}
}
func (p *MemoryProvider) dumpAdmins() ([]Admin, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
admins := make([]Admin, 0, len(p.dbHandle.admins))
if p.dbHandle.isClosed {
return admins, errMemoryProviderClosed
}
for _, admin := range p.dbHandle.admins {
admins = append(admins, admin)
}
return admins, nil
}
func (p *MemoryProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
admins := make([]Admin, 0, limit)
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return admins, errMemoryProviderClosed
}
if limit <= 0 {
return admins, nil
}
itNum := 0
if order == OrderASC {
for _, username := range p.dbHandle.adminsUsernames {
itNum++
if itNum <= offset {
continue
}
a := p.dbHandle.admins[username]
admin := a.getACopy()
admin.HideConfidentialData()
admins = append(admins, admin)
if len(admins) >= limit {
break
}
}
} else {
for i := len(p.dbHandle.adminsUsernames) - 1; i >= 0; i-- {
itNum++
if itNum <= offset {
continue
}
username := p.dbHandle.adminsUsernames[i]
a := p.dbHandle.admins[username]
admin := a.getACopy()
admin.HideConfidentialData()
admins = append(admins, admin)
if len(admins) >= limit {
break
}
}
}
return admins, nil
}
func (p *MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -383,7 +511,7 @@ func (p MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeA
return nil
}
func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
func (p *MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -397,7 +525,7 @@ func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
}
func (p MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder {
func (p *MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder {
var folders []vfs.VirtualFolder
for _, folder := range user.VirtualFolders {
f, err := p.addOrGetFolderInternal(folder.MappedPath, user.Username, folder.UsedQuotaSize, folder.UsedQuotaFiles,
@ -413,7 +541,7 @@ func (p MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder
return folders
}
func (p MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) {
func (p *MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) {
folder, err := p.folderExistsInternal(mappedPath)
if err == nil {
var usernames []string
@ -427,7 +555,7 @@ func (p MemoryProvider) removeUserFromFolderMapping(mappedPath, username string)
}
}
func (p MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) {
func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) {
p.dbHandle.vfolders[folder.MappedPath] = folder
if !utils.IsStringInSlice(folder.MappedPath, p.dbHandle.vfoldersPaths) {
p.dbHandle.vfoldersPaths = append(p.dbHandle.vfoldersPaths, folder.MappedPath)
@ -435,7 +563,7 @@ func (p MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolde
}
}
func (p MemoryProvider) addOrGetFolderInternal(mappedPath, username string, usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64) (vfs.BaseVirtualFolder, error) {
func (p *MemoryProvider) addOrGetFolderInternal(mappedPath, username string, usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64) (vfs.BaseVirtualFolder, error) {
folder, err := p.folderExistsInternal(mappedPath)
if _, ok := err.(*RecordNotFoundError); ok {
folder := vfs.BaseVirtualFolder{
@ -456,14 +584,14 @@ func (p MemoryProvider) addOrGetFolderInternal(mappedPath, username string, used
return folder, err
}
func (p MemoryProvider) folderExistsInternal(mappedPath string) (vfs.BaseVirtualFolder, error) {
func (p *MemoryProvider) folderExistsInternal(mappedPath string) (vfs.BaseVirtualFolder, error) {
if val, ok := p.dbHandle.vfolders[mappedPath]; ok {
return val, nil
}
return vfs.BaseVirtualFolder{}, &RecordNotFoundError{err: fmt.Sprintf("folder %#v does not exist", mappedPath)}
}
func (p MemoryProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
func (p *MemoryProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
folders := make([]vfs.BaseVirtualFolder, 0, limit)
var err error
p.dbHandle.Lock()
@ -514,7 +642,7 @@ func (p MemoryProvider) getFolders(limit, offset int, order, folderPath string)
return folders, err
}
func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
func (p *MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -523,7 +651,7 @@ func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolde
return p.folderExistsInternal(mappedPath)
}
func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -544,7 +672,7 @@ func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return nil
}
func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
@ -577,17 +705,17 @@ func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return nil
}
func (p MemoryProvider) getNextID() int64 {
func (p *MemoryProvider) getNextID() int64 {
nextID := int64(1)
for id := range p.dbHandle.usersIdx {
if id >= nextID {
nextID = id + 1
for _, v := range p.dbHandle.users {
if v.ID >= nextID {
nextID = v.ID + 1
}
}
return nextID
}
func (p MemoryProvider) getNextFolderID() int64 {
func (p *MemoryProvider) getNextFolderID() int64 {
nextID := int64(1)
for _, v := range p.dbHandle.vfolders {
if v.ID >= nextID {
@ -597,17 +725,28 @@ func (p MemoryProvider) getNextFolderID() int64 {
return nextID
}
func (p MemoryProvider) clear() {
func (p *MemoryProvider) getNextAdminID() int64 {
nextID := int64(1)
for _, a := range p.dbHandle.admins {
if a.ID >= nextID {
nextID = a.ID + 1
}
}
return nextID
}
func (p *MemoryProvider) clear() {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
p.dbHandle.usernames = []string{}
p.dbHandle.usersIdx = make(map[int64]string)
p.dbHandle.users = make(map[string]User)
p.dbHandle.vfoldersPaths = []string{}
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)
p.dbHandle.admins = make(map[string]Admin)
p.dbHandle.adminsUsernames = []string{}
}
func (p MemoryProvider) reloadConfig() error {
func (p *MemoryProvider) reloadConfig() error {
if p.dbHandle.configFile == "" {
providerLog(logger.LevelDebug, "no users configuration file defined")
return nil
@ -676,14 +815,14 @@ func (p MemoryProvider) reloadConfig() error {
}
// initializeDatabase does nothing, no initilization is needed for memory provider
func (p MemoryProvider) initializeDatabase() error {
func (p *MemoryProvider) initializeDatabase() error {
return ErrNoInitRequired
}
func (p MemoryProvider) migrateDatabase() error {
func (p *MemoryProvider) migrateDatabase() error {
return ErrNoInitRequired
}
func (p MemoryProvider) revertDatabase(targetVersion int) error {
func (p *MemoryProvider) revertDatabase(targetVersion int) error {
return errors.New("memory provider does not store data, revert not possible")
}

View file

@ -40,6 +40,10 @@ const (
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;"
mysqlV6SQL = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;"
mysqlV6DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `additional_info`;"
mysqlV7SQL = "CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
"`password` varchar(255) NOT NULL, `email` varchar(255) NULL, `status` integer NOT NULL, `permissions` longtext NOT NULL, " +
"`filters` longtext NULL, `additional_info` longtext NULL);"
mysqlV7DownSQL = "DROP TABLE `{{admins}}` CASCADE;"
)
// MySQLProvider auth provider for MySQL/MariaDB database
@ -65,7 +69,7 @@ func initializeMySQLProvider() error {
dbHandle.SetMaxIdleConns(2)
}
dbHandle.SetConnMaxLifetime(240 * time.Second)
provider = MySQLProvider{dbHandle: dbHandle}
provider = &MySQLProvider{dbHandle: dbHandle}
} else {
providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v",
getMySQLConnectionString(true), err)
@ -87,98 +91,122 @@ func getMySQLConnectionString(redactedPwd bool) string {
return connectionString
}
func (p MySQLProvider) checkAvailability() error {
func (p *MySQLProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle)
}
func (p MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
func (p *MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
}
func (p MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
func (p *MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
}
func (p MySQLProvider) getUserByID(ID int64) (User, error) {
return sqlCommonGetUserByID(ID, p.dbHandle)
}
func (p MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p MySQLProvider) getUsedQuota(username string) (int, int64, error) {
func (p *MySQLProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p MySQLProvider) updateLastLogin(username string) error {
func (p *MySQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p MySQLProvider) userExists(username string) (User, error) {
return sqlCommonCheckUserExists(username, p.dbHandle)
func (p *MySQLProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
func (p MySQLProvider) addUser(user *User) error {
func (p *MySQLProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle)
}
func (p MySQLProvider) updateUser(user *User) error {
func (p *MySQLProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle)
}
func (p MySQLProvider) deleteUser(user *User) error {
func (p *MySQLProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle)
}
func (p MySQLProvider) dumpUsers() ([]User, error) {
func (p *MySQLProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle)
}
func (p MySQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
func (p *MySQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
}
func (p MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
func (p *MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return sqlCommonDumpFolders(p.dbHandle)
}
func (p MySQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
func (p *MySQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle)
}
func (p MySQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
func (p *MySQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle)
}
func (p MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
func (p *MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonAddFolder(folder, p.dbHandle)
}
func (p MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
func (p *MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonDeleteFolder(folder, p.dbHandle)
}
func (p MySQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *MySQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p MySQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
func (p *MySQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle)
}
func (p MySQLProvider) close() error {
func (p *MySQLProvider) adminExists(username string) (Admin, error) {
return sqlCommonGetAdminByUsername(username, p.dbHandle)
}
func (p *MySQLProvider) addAdmin(admin *Admin) error {
return sqlCommonAddAdmin(admin, p.dbHandle)
}
func (p *MySQLProvider) updateAdmin(admin *Admin) error {
return sqlCommonUpdateAdmin(admin, p.dbHandle)
}
func (p *MySQLProvider) deleteAdmin(admin *Admin) error {
return sqlCommonDeleteAdmin(admin, p.dbHandle)
}
func (p *MySQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
}
func (p *MySQLProvider) dumpAdmins() ([]Admin, error) {
return sqlCommonDumpAdmins(p.dbHandle)
}
func (p *MySQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
}
func (p *MySQLProvider) close() error {
return p.dbHandle.Close()
}
func (p MySQLProvider) reloadConfig() error {
func (p *MySQLProvider) reloadConfig() error {
return nil
}
// initializeDatabase creates the initial database structure
func (p MySQLProvider) initializeDatabase() error {
func (p *MySQLProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired
@ -206,7 +234,7 @@ func (p MySQLProvider) initializeDatabase() error {
return tx.Commit()
}
func (p MySQLProvider) migrateDatabase() error {
func (p *MySQLProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -226,6 +254,8 @@ func (p MySQLProvider) migrateDatabase() error {
return updateMySQLDatabaseFromV4(p.dbHandle)
case 5:
return updateMySQLDatabaseFromV5(p.dbHandle)
case 6:
return updateMySQLDatabaseFromV6(p.dbHandle)
default:
if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
@ -238,7 +268,7 @@ func (p MySQLProvider) migrateDatabase() error {
}
}
func (p MySQLProvider) revertDatabase(targetVersion int) error {
func (p *MySQLProvider) revertDatabase(targetVersion int) error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -247,6 +277,16 @@ func (p MySQLProvider) revertDatabase(targetVersion int) error {
return fmt.Errorf("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 7:
err = downgradeMySQLDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
case 6:
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
@ -293,7 +333,15 @@ func updateMySQLDatabaseFromV4(dbHandle *sql.DB) error {
}
func updateMySQLDatabaseFromV5(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom5To6(dbHandle)
err := updateMySQLDatabaseFrom5To6(dbHandle)
if err != nil {
return err
}
return updateMySQLDatabaseFromV6(dbHandle)
}
func updateMySQLDatabaseFromV6(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom6To7(dbHandle)
}
func updateMySQLDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -325,6 +373,20 @@ func updateMySQLDatabaseFrom5To6(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func updateMySQLDatabaseFrom6To7(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 6 -> 7")
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
sql := strings.Replace(mysqlV7SQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func downgradeMySQLDatabaseFrom7To6(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 7 -> 6")
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
sql := strings.Replace(mysqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func downgradeMySQLDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")

View file

@ -40,6 +40,11 @@ CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
`
pgsqlV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
pgsqlV6DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "additional_info" CASCADE;`
pgsqlV7SQL = `CREATE TABLE "{{admins}}" ("id" serial NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE,
"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL,
"filters" text NULL, "additional_info" text NULL);
`
pgsqlV7DownSQL = `DROP TABLE "{{admins}}" CASCADE;`
)
// PGSQLProvider auth provider for PostgreSQL database
@ -65,7 +70,7 @@ func initializePGSQLProvider() error {
dbHandle.SetMaxIdleConns(2)
}
dbHandle.SetConnMaxLifetime(240 * time.Second)
provider = PGSQLProvider{dbHandle: dbHandle}
provider = &PGSQLProvider{dbHandle: dbHandle}
} else {
providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v",
getPGSQLConnectionString(true), err)
@ -88,98 +93,122 @@ func getPGSQLConnectionString(redactedPwd bool) string {
return connectionString
}
func (p PGSQLProvider) checkAvailability() error {
func (p *PGSQLProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle)
}
func (p PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
func (p *PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
}
func (p PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
func (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
}
func (p PGSQLProvider) getUserByID(ID int64) (User, error) {
return sqlCommonGetUserByID(ID, p.dbHandle)
}
func (p PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p PGSQLProvider) updateLastLogin(username string) error {
func (p *PGSQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p PGSQLProvider) userExists(username string) (User, error) {
return sqlCommonCheckUserExists(username, p.dbHandle)
func (p *PGSQLProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
func (p PGSQLProvider) addUser(user *User) error {
func (p *PGSQLProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle)
}
func (p PGSQLProvider) updateUser(user *User) error {
func (p *PGSQLProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle)
}
func (p PGSQLProvider) deleteUser(user *User) error {
func (p *PGSQLProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle)
}
func (p PGSQLProvider) dumpUsers() ([]User, error) {
func (p *PGSQLProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle)
}
func (p PGSQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
func (p *PGSQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
}
func (p PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
func (p *PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return sqlCommonDumpFolders(p.dbHandle)
}
func (p PGSQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
func (p *PGSQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle)
}
func (p PGSQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
func (p *PGSQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle)
}
func (p PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
func (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonAddFolder(folder, p.dbHandle)
}
func (p PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
func (p *PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonDeleteFolder(folder, p.dbHandle)
}
func (p PGSQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *PGSQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p PGSQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
func (p *PGSQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle)
}
func (p PGSQLProvider) close() error {
func (p *PGSQLProvider) adminExists(username string) (Admin, error) {
return sqlCommonGetAdminByUsername(username, p.dbHandle)
}
func (p *PGSQLProvider) addAdmin(admin *Admin) error {
return sqlCommonAddAdmin(admin, p.dbHandle)
}
func (p *PGSQLProvider) updateAdmin(admin *Admin) error {
return sqlCommonUpdateAdmin(admin, p.dbHandle)
}
func (p *PGSQLProvider) deleteAdmin(admin *Admin) error {
return sqlCommonDeleteAdmin(admin, p.dbHandle)
}
func (p *PGSQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
}
func (p *PGSQLProvider) dumpAdmins() ([]Admin, error) {
return sqlCommonDumpAdmins(p.dbHandle)
}
func (p *PGSQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
}
func (p *PGSQLProvider) close() error {
return p.dbHandle.Close()
}
func (p PGSQLProvider) reloadConfig() error {
func (p *PGSQLProvider) reloadConfig() error {
return nil
}
// initializeDatabase creates the initial database structure
func (p PGSQLProvider) initializeDatabase() error {
func (p *PGSQLProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired
@ -207,7 +236,7 @@ func (p PGSQLProvider) initializeDatabase() error {
return tx.Commit()
}
func (p PGSQLProvider) migrateDatabase() error {
func (p *PGSQLProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -227,6 +256,8 @@ func (p PGSQLProvider) migrateDatabase() error {
return updatePGSQLDatabaseFromV4(p.dbHandle)
case 5:
return updatePGSQLDatabaseFromV5(p.dbHandle)
case 6:
return updatePGSQLDatabaseFromV6(p.dbHandle)
default:
if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
@ -239,7 +270,7 @@ func (p PGSQLProvider) migrateDatabase() error {
}
}
func (p PGSQLProvider) revertDatabase(targetVersion int) error {
func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -248,6 +279,16 @@ func (p PGSQLProvider) revertDatabase(targetVersion int) error {
return fmt.Errorf("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 7:
err = downgradePGSQLDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
case 6:
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
if err != nil {
@ -294,7 +335,15 @@ func updatePGSQLDatabaseFromV4(dbHandle *sql.DB) error {
}
func updatePGSQLDatabaseFromV5(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom5To6(dbHandle)
err := updatePGSQLDatabaseFrom5To6(dbHandle)
if err != nil {
return err
}
return updatePGSQLDatabaseFromV6(dbHandle)
}
func updatePGSQLDatabaseFromV6(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom6To7(dbHandle)
}
func updatePGSQLDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -326,6 +375,20 @@ func updatePGSQLDatabaseFrom5To6(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func updatePGSQLDatabaseFrom6To7(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 6 -> 7")
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
sql := strings.Replace(pgsqlV7SQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func downgradePGSQLDatabaseFrom7To6(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 7 -> 6")
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
sql := strings.Replace(pgsqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func downgradePGSQLDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")

View file

@ -14,7 +14,7 @@ import (
)
const (
sqlDatabaseVersion = 6
sqlDatabaseVersion = 7
initialDBVersionSQL = "INSERT INTO {{schema_version}} (version) VALUES (1);"
defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second
@ -26,7 +26,174 @@ type sqlQuerier interface {
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
}
func getUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
type sqlScanner interface {
Scan(dest ...interface{}) error
}
func sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, error) {
var admin Admin
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAdminByUsernameQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return admin, err
}
defer stmt.Close()
row := stmt.QueryRowContext(ctx, username)
return getAdminFromDbRow(row)
}
func sqlCommonValidateAdminAndPass(username, password, ip string, dbHandle *sql.DB) (Admin, error) {
admin, err := sqlCommonGetAdminByUsername(username, dbHandle)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
return admin, err
}
err = admin.checkUserAndPass(password, ip)
return admin, err
}
func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
err := admin.validate()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAddAdminQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return err
}
defer stmt.Close()
perms, err := json.Marshal(admin.Permissions)
if err != nil {
return err
}
filters, err := json.Marshal(admin.Filters)
if err != nil {
return err
}
_, err = stmt.ExecContext(ctx, admin.Username, admin.Password, admin.Status, admin.Email, string(perms),
string(filters), admin.AdditionalInfo)
return err
}
func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
err := admin.validate()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUpdateAdminQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return err
}
defer stmt.Close()
perms, err := json.Marshal(admin.Permissions)
if err != nil {
return err
}
filters, err := json.Marshal(admin.Filters)
if err != nil {
return err
}
_, err = stmt.ExecContext(ctx, admin.Password, admin.Status, admin.Email, string(perms), string(filters),
admin.AdditionalInfo, admin.Username)
return err
}
func sqlCommonDeleteAdmin(admin *Admin, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getDeleteAdminQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return err
}
defer stmt.Close()
_, err = stmt.ExecContext(ctx, admin.Username)
return err
}
func sqlCommonGetAdmins(limit, offset int, order string, dbHandle sqlQuerier) ([]Admin, error) {
admins := make([]Admin, 0, limit)
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAdminsQuery(order)
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return nil, err
}
defer stmt.Close()
rows, err := stmt.QueryContext(ctx, limit, offset)
if err != nil {
return admins, err
}
defer rows.Close()
for rows.Next() {
a, err := getAdminFromDbRow(rows)
if err != nil {
return admins, err
}
a.HideConfidentialData()
admins = append(admins, a)
}
return admins, rows.Err()
}
func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
admins := make([]Admin, 0, 30)
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getDumpAdminsQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return nil, err
}
defer stmt.Close()
rows, err := stmt.QueryContext(ctx)
if err != nil {
return admins, err
}
defer rows.Close()
for rows.Next() {
a, err := getAdminFromDbRow(rows)
if err != nil {
return admins, err
}
admins = append(admins, a)
}
return admins, rows.Err()
}
func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
var user User
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
@ -39,7 +206,7 @@ func getUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
defer stmt.Close()
row := stmt.QueryRowContext(ctx, username)
user, err = getUserFromDbRow(row, nil)
user, err = getUserFromDbRow(row)
if err != nil {
return user, err
}
@ -51,7 +218,7 @@ func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHan
if password == "" {
return user, errors.New("Credentials cannot be null or empty")
}
user, err := getUserByUsername(username, dbHandle)
user, err := sqlCommonGetUserByUsername(username, dbHandle)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, err
@ -64,7 +231,7 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, dbHandle *sq
if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty")
}
user, err := getUserByUsername(username, dbHandle)
user, err := sqlCommonGetUserByUsername(username, dbHandle)
if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, "", err
@ -78,26 +245,6 @@ func sqlCommonCheckAvailability(dbHandle *sql.DB) error {
return dbHandle.PingContext(ctx)
}
func sqlCommonGetUserByID(ID int64, dbHandle *sql.DB) (User, error) {
var user User
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUserByIDQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return user, err
}
defer stmt.Close()
row := stmt.QueryRowContext(ctx, ID)
user, err = getUserFromDbRow(row, nil)
if err != nil {
return user, err
}
return getUserWithVirtualFolders(user, dbHandle)
}
func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
@ -158,25 +305,6 @@ func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
return err
}
func sqlCommonCheckUserExists(username string, dbHandle *sql.DB) (User, error) {
var user User
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUserByUsernameQuery()
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return user, err
}
defer stmt.Close()
row := stmt.QueryRowContext(ctx, username)
user, err = getUserFromDbRow(row, nil)
if err != nil {
return user, err
}
return getUserWithVirtualFolders(user, dbHandle)
}
func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
err := validateUser(user)
if err != nil {
@ -317,7 +445,7 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {
defer rows.Close()
for rows.Next() {
u, err := getUserFromDbRow(nil, rows)
u, err := getUserFromDbRow(rows)
if err != nil {
return users, err
}
@ -327,30 +455,30 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {
}
users = append(users, u)
}
err = rows.Err()
if err != nil {
return users, err
}
return getUsersWithVirtualFolders(users, dbHandle)
}
func sqlCommonGetUsers(limit int, offset int, order string, username string, dbHandle sqlQuerier) ([]User, error) {
func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier) ([]User, error) {
users := make([]User, 0, limit)
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUsersQuery(order, username)
q := getUsersQuery(order)
stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return nil, err
}
defer stmt.Close()
var rows *sql.Rows
if len(username) > 0 {
rows, err = stmt.QueryContext(ctx, username, limit, offset) //nolint:rowserrcheck // rows.Err() is checked
} else {
rows, err = stmt.QueryContext(ctx, limit, offset) //nolint:rowserrcheck // rows.Err() is checked
}
rows, err := stmt.QueryContext(ctx, limit, offset)
if err == nil {
defer rows.Close()
for rows.Next() {
u, err := getUserFromDbRow(nil, rows)
u, err := getUserFromDbRow(rows)
if err != nil {
return users, err
}
@ -384,7 +512,47 @@ func updateUserPermissionsFromDb(user *User, permissions string) error {
return err
}
func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
func getAdminFromDbRow(row sqlScanner) (Admin, error) {
var admin Admin
var email, filters, additionalInfo, permissions sql.NullString
err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,
&filters, &additionalInfo)
if err != nil {
if err == sql.ErrNoRows {
return admin, &RecordNotFoundError{err: err.Error()}
}
return admin, err
}
if permissions.Valid {
var perms []string
err = json.Unmarshal([]byte(permissions.String), &perms)
if err != nil {
return admin, err
}
admin.Permissions = perms
}
if email.Valid {
admin.Email = email.String
}
if filters.Valid {
var adminFilters AdminFilters
err = json.Unmarshal([]byte(filters.String), &adminFilters)
if err == nil {
admin.Filters = adminFilters
}
}
if additionalInfo.Valid {
admin.AdditionalInfo = additionalInfo.String
}
return admin, err
}
func getUserFromDbRow(row sqlScanner) (User, error) {
var user User
var permissions sql.NullString
var password sql.NullString
@ -392,18 +560,11 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
var filters sql.NullString
var fsConfig sql.NullString
var additionalInfo sql.NullString
var err error
if row != nil {
err = row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
&additionalInfo)
} else {
err = rows.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
&additionalInfo)
}
err := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
&additionalInfo)
if err != nil {
if err == sql.ErrNoRows {
return user, &RecordNotFoundError{err: err.Error()}

View file

@ -78,6 +78,10 @@ INSERT INTO "new__users" ("id", "username", "password", "public_keys", "home_dir
DROP TABLE "{{users}}";
ALTER TABLE "new__users" RENAME TO "{{users}}";
`
sqliteV7SQL = `CREATE TABLE "{{admins}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE,
"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL, "filters" text NULL,
"additional_info" text NULL);`
sqliteV7DownSQL = `DROP TABLE "{{admins}}";`
)
// SQLiteProvider auth provider for SQLite database
@ -109,7 +113,7 @@ func initializeSQLiteProvider(basePath string) error {
if err == nil {
providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %#v", connectionString)
dbHandle.SetMaxOpenConns(1)
provider = SQLiteProvider{dbHandle: dbHandle}
provider = &SQLiteProvider{dbHandle: dbHandle}
} else {
providerLog(logger.LevelWarn, "error creating sqlite database handler, connection string: %#v, error: %v",
connectionString, err)
@ -117,98 +121,122 @@ func initializeSQLiteProvider(basePath string) error {
return err
}
func (p SQLiteProvider) checkAvailability() error {
func (p *SQLiteProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle)
}
func (p SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
func (p *SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
}
func (p SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
func (p *SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
}
func (p SQLiteProvider) getUserByID(ID int64) (User, error) {
return sqlCommonGetUserByID(ID, p.dbHandle)
}
func (p SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p SQLiteProvider) getUsedQuota(username string) (int, int64, error) {
func (p *SQLiteProvider) getUsedQuota(username string) (int, int64, error) {
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p SQLiteProvider) updateLastLogin(username string) error {
func (p *SQLiteProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle)
}
func (p SQLiteProvider) userExists(username string) (User, error) {
return sqlCommonCheckUserExists(username, p.dbHandle)
func (p *SQLiteProvider) userExists(username string) (User, error) {
return sqlCommonGetUserByUsername(username, p.dbHandle)
}
func (p SQLiteProvider) addUser(user *User) error {
func (p *SQLiteProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle)
}
func (p SQLiteProvider) updateUser(user *User) error {
func (p *SQLiteProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle)
}
func (p SQLiteProvider) deleteUser(user *User) error {
func (p *SQLiteProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle)
}
func (p SQLiteProvider) dumpUsers() ([]User, error) {
func (p *SQLiteProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle)
}
func (p SQLiteProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
func (p *SQLiteProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
}
func (p SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
func (p *SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return sqlCommonDumpFolders(p.dbHandle)
}
func (p SQLiteProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
func (p *SQLiteProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle)
}
func (p SQLiteProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
func (p *SQLiteProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle)
}
func (p SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
func (p *SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonAddFolder(folder, p.dbHandle)
}
func (p SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
func (p *SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return sqlCommonDeleteFolder(folder, p.dbHandle)
}
func (p SQLiteProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
func (p *SQLiteProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle)
}
func (p SQLiteProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
func (p *SQLiteProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle)
}
func (p SQLiteProvider) close() error {
func (p *SQLiteProvider) adminExists(username string) (Admin, error) {
return sqlCommonGetAdminByUsername(username, p.dbHandle)
}
func (p *SQLiteProvider) addAdmin(admin *Admin) error {
return sqlCommonAddAdmin(admin, p.dbHandle)
}
func (p *SQLiteProvider) updateAdmin(admin *Admin) error {
return sqlCommonUpdateAdmin(admin, p.dbHandle)
}
func (p *SQLiteProvider) deleteAdmin(admin *Admin) error {
return sqlCommonDeleteAdmin(admin, p.dbHandle)
}
func (p *SQLiteProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
}
func (p *SQLiteProvider) dumpAdmins() ([]Admin, error) {
return sqlCommonDumpAdmins(p.dbHandle)
}
func (p *SQLiteProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
}
func (p *SQLiteProvider) close() error {
return p.dbHandle.Close()
}
func (p SQLiteProvider) reloadConfig() error {
func (p *SQLiteProvider) reloadConfig() error {
return nil
}
// initializeDatabase creates the initial database structure
func (p SQLiteProvider) initializeDatabase() error {
func (p *SQLiteProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired
@ -236,7 +264,7 @@ func (p SQLiteProvider) initializeDatabase() error {
return tx.Commit()
}
func (p SQLiteProvider) migrateDatabase() error {
func (p *SQLiteProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -256,6 +284,8 @@ func (p SQLiteProvider) migrateDatabase() error {
return updateSQLiteDatabaseFromV4(p.dbHandle)
case 5:
return updateSQLiteDatabaseFromV5(p.dbHandle)
case 6:
return updateSQLiteDatabaseFromV6(p.dbHandle)
default:
if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
@ -268,7 +298,7 @@ func (p SQLiteProvider) migrateDatabase() error {
}
}
func (p SQLiteProvider) revertDatabase(targetVersion int) error {
func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -277,6 +307,16 @@ func (p SQLiteProvider) revertDatabase(targetVersion int) error {
return fmt.Errorf("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 7:
err = downgradeSQLiteDatabaseFrom7To6(p.dbHandle)
if err != nil {
return err
}
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
if err != nil {
return err
}
return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
case 6:
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
if err != nil {
@ -323,7 +363,15 @@ func updateSQLiteDatabaseFromV4(dbHandle *sql.DB) error {
}
func updateSQLiteDatabaseFromV5(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom5To6(dbHandle)
err := updateSQLiteDatabaseFrom5To6(dbHandle)
if err != nil {
return err
}
return updateSQLiteDatabaseFromV6(dbHandle)
}
func updateSQLiteDatabaseFromV6(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom6To7(dbHandle)
}
func updateSQLiteDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -355,6 +403,20 @@ func updateSQLiteDatabaseFrom5To6(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func updateSQLiteDatabaseFrom6To7(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 6 -> 7")
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
sql := strings.Replace(sqliteV7SQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
}
func downgradeSQLiteDatabaseFrom7To6(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 7 -> 6")
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
sql := strings.Replace(sqliteV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
}
func downgradeSQLiteDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")

View file

@ -12,6 +12,7 @@ const (
selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," +
"used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem,additional_info"
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update"
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info"
)
func getSQLPlaceholders() []string {
@ -26,19 +27,40 @@ func getSQLPlaceholders() []string {
return placeholders
}
func getAdminByUsernameQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectAdminFields, sqlTableAdmins, sqlPlaceholders[0])
}
func getAdminsQuery(order string) string {
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectAdminFields, sqlTableAdmins,
order, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getDumpAdminsQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v`, selectAdminFields, sqlTableAdmins)
}
func getAddAdminQuery() string {
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info)
VALUES (%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
}
func getUpdateAdminQuery() string {
return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v
WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
}
func getDeleteAdminQuery() string {
return fmt.Sprintf(`DELETE FROM %v WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0])
}
func getUserByUsernameQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
}
func getUserByIDQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v WHERE id = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
}
func getUsersQuery(order string, username string) string {
if len(username) > 0 {
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v ORDER BY username %v LIMIT %v OFFSET %v`,
selectUserFields, sqlTableUsers, sqlPlaceholders[0], order, sqlPlaceholders[1], sqlPlaceholders[2])
}
func getUsersQuery(order string) string {
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectUserFields, sqlTableUsers,
order, sqlPlaceholders[0], sqlPlaceholders[1])
}

View file

@ -20,7 +20,7 @@ import (
"github.com/drakkan/sftpgo/vfs"
)
// Available permissions for SFTP users
// Available permissions for SFTPGo users
const (
// All permissions are granted
PermAny = "*"
@ -802,26 +802,12 @@ func (u *User) GetExpirationDateAsString() string {
// GetAllowedIPAsString returns the allowed IP as comma separated string
func (u User) GetAllowedIPAsString() string {
result := ""
for _, IPMask := range u.Filters.AllowedIP {
if len(result) > 0 {
result += ","
}
result += IPMask
}
return result
return strings.Join(u.Filters.AllowedIP, ",")
}
// GetDeniedIPAsString returns the denied IP as comma separated string
func (u User) GetDeniedIPAsString() string {
result := ""
for _, IPMask := range u.Filters.DeniedIP {
if len(result) > 0 {
result += ","
}
result += IPMask
}
return result
return strings.Join(u.Filters.DeniedIP, ",")
}
// SetEmptySecretsIfNil sets the secrets to empty if nil

View file

@ -15,5 +15,5 @@ SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha5
If you want to use your existing accounts, you have these options:
- you can import your users inside SFTPGo. Take a look at [sftpgo_api_cli](../examples/rest-api-cli#convert-users-from-other-stores "SFTPGo API CLI example"), it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
- you can import your users inside SFTPGo. Take a look at [convert users](.../examples/convertusers) script, it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
- you can use an external authentication program

View file

@ -38,7 +38,7 @@ The `defender` can also load a permanent block list and/or a safe list of ip add
- `safelist_file`, defines the path to a file containing a list of ip addresses and/or networks to never ban.
- `blocklist_file`, defines the path to a file containing a list of ip addresses and/or networks to always ban.
These list must be stored as JSON with the following schema:
These list must be stored as JSON conforming to the following schema:
- `addresses`, list of strings. Each string must be a valid IPv4/IPv6 address.
- `networks`, list of strings. Each string must be a valid IPv4/IPv6 CIDR address.

View file

@ -160,7 +160,6 @@ The configuration file contains the following sections:
- `sslmode`, integer. 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`
- `connectionstring`, string. Provide a custom database connection string. If not empty, this connection string will be used instead of building one using the previous parameters. Leave empty for drivers `bolt` and `memory`
- `sql_tables_prefix`, string. Prefix for SQL tables
- `manage_users`, integer. Set to 0 to disable users management, 1 to enable
- `track_quota`, integer. Set the preferred mode to track users quota between the following choices:
- 0, disable quota tracking. REST API to scan users home directories/virtual folders and update quota will do nothing
- 1, quota is updated each time a user uploads or deletes a file, even if the user has no quota restrictions
@ -193,7 +192,6 @@ The configuration file contains the following sections:
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
- `auth_user_file`, string. Path to a file used to store usernames and passwords for basic authentication. This can be an absolute path or a path relative to the config dir. We support HTTP basic authentication, and the file format must conform to the one generated using the Apache `htpasswd` tool. The supported password formats are bcrypt (`$2y$` prefix) and md5 crypt (`$apr1$` prefix). If empty, HTTP authentication is disabled.
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server)

View file

@ -4,32 +4,40 @@ SFTPGo exposes REST API to manage, backup, and restore users and folders, and to
If quota tracking is enabled in the configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP/SCP, or if you change `track_quota` from `2` to `1`, you can rescan the users home dir and update the used quota using the REST API.
REST API can be protected using HTTP basic authentication and exposed via HTTPS. If you need more advanced security features, you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
REST API are protected using JSON Web Tokens (JWT) authentication and can be exposed over HTTPS.
For example, you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way:
The default credentials are:
```shell
ProxyPass /api/v1 http://127.0.0.1:8080/api/v1
ProxyPassReverse /api/v1 http://127.0.0.1:8080/api/v1
- username: `admin`
- password: `password`
You can get a JWT token using the `/api/v2/token` endpoint, you need to authenticate using HTTP Basic authentication and the credentials of an active administrator. Here is a sample response:
```json
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTA4NzU5NDksImp0aSI6ImMwMjAzbGZjZHJwZDRsMGMxanZnIiwibmJmIjoxNjEwODc1MzE5LCJwZXJtaXNzaW9ucyI6WyIqIl0sInN1YiI6ImlHZ010NlZNU3AzN2tld3hMR3lUV1l2b2p1a2ttSjBodXlJZHBzSWRyOFE9IiwidXNlcm5hbWUiOiJhZG1pbiJ9.dt-UwcWdEMwoGauuiQw8BmgpBAv4YlTaXkyNK-7iRJ4","expires_at":"2021-01-17T09:32:29Z"}
```
and you can add authentication with something like this:
once the access token has expired, you need to get a new one.
```shell
<Location /api/v1>
AuthType Digest
AuthName "Private"
AuthDigestDomain "/api/v1"
AuthDigestProvider file
AuthUserFile "/etc/httpd/conf/auth_digest"
Require valid-user
</Location>
```
JWT tokens are not stored and we use a randomly generated secret to sign them so if you restart SFTPGo all the previous tokens will be invalidated and you will get a 401 HTTP response code.
and, of course, you can configure the web server to use HTTPS.
You can create other administrator and assign them the following permissions:
- add users
- edit users
- del users
- view users
- view connections
- close connections
- view server status
- view and start quota scans
- view defender
- manage defender
- manage system
- manage admins
You can also restrict administrator access based on the source IP address. If you are running SFTPGo behind a reverse proxy you need to allow both the proxy IP address and the real client IP.
The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](../httpd/schema/openapi.yaml "OpenAPI 3 specs").
A sample CLI client for the REST API can be found inside the source tree [rest-api-cli](../examples/rest-api-cli) directory.
You can also generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/)
You can generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/).

View file

@ -61,8 +61,6 @@ sudo systemctl start sftpgo
sudo systemctl status sftpgo
# automatically start sftpgo on boot
sudo systemctl enable sftpgo
# optional, install the REST API CLI. It requires python-requests to run
sudo install -Dm755 examples/rest-api-cli/sftpgo_api_cli /usr/bin/sftpgo_api_cli
# optional, create shell completion script, for example for bash
sudo sh -c '/usr/bin/sftpgo gen completion bash > /usr/share/bash-completion/completions/sftpgo'
# optional, create man pages
@ -102,8 +100,6 @@ sudo ln -s /usr/local/opt/sftpgo/init/com.github.drakkan.sftpgo.plist /Library/L
sudo launchctl load -w /Library/LaunchDaemons/com.github.drakkan.sftpgo.plist
# verify that the service is started
sudo launchctl list com.github.drakkan.sftpgo
# optional, install the REST API CLI. It requires python-requests to run, this python module is not installed by default
sudo cp examples/rest-api-cli/sftpgo_api_cli /usr/local/opt/sftpgo/bin/
```
## Windows

View file

@ -1,8 +1,13 @@
# Web Admin
You can easily build your own interface using the exposed REST API. Anyway, SFTPGo also provides a very basic built-in web interface that allows you to manage users and connections.
You can easily build your own interface using the exposed [REST API](./rest-api.md). Anyway, SFTPGo also provides a basic built-in web interface that allows you to manage users, virtual folders, admins and connections.
With the default `httpd` configuration, the web admin is available at the following URL:
[http://127.0.0.1:8080/web](http://127.0.0.1:8080/web)
The web interface can be protected using HTTP basic authentication and exposed via HTTPS. If you need more advanced security features, you can setup a reverse proxy as explained for the [REST API](./rest-api.md).
The default credentials are:
- username: `admin`
- password: `password`
The web interface can be exposed over HTTPS.

View file

@ -0,0 +1,49 @@
# Import users from other stores
`convertusers` is a very simple command line client, written in python, to import users from other stores. It requires `python3` or `python2`.
Here is the usage:
```console
usage: convertusers [-h] [--min-uid MIN_UID] [--max-uid MAX_UID] [--usernames USERNAMES [USERNAMES ...]]
[--force-uid FORCE_UID] [--force-gid FORCE_GID]
input_file {unix-passwd,pure-ftpd,proftpd} output_file
Convert users to a JSON format suitable to use with loadddata
positional arguments:
input_file
{unix-passwd,pure-ftpd,proftpd}
To import from unix-passwd format you need the permission to read /etc/shadow that is typically
granted to the root user only
output_file
optional arguments:
-h, --help show this help message and exit
--min-uid MIN_UID if >= 0 only import users with UID greater or equal to this value. Default: -1
--max-uid MAX_UID if >= 0 only import users with UID lesser or equal to this value. Default: -1
--usernames USERNAMES [USERNAMES ...]
Only import users with these usernames. Default: []
--force-uid FORCE_UID
if >= 0 the imported users will have this UID in SFTPGo. Default: -1
--force-gid FORCE_GID
if >= 0 the imported users will have this GID in SFTPGo. Default: -1
```
Let's see some examples:
```console
python convertusers "" unix-passwd unix_users.json --min-uid 500 --force-uid 1000 --force-gid 1000
```
```console
python convertusers pureftpd.passwd pure-ftpd pure_users.json --usernames "user1" "user2"
```
```console
python convertusers proftpd.passwd proftpd pro_users.json
```
The generated json file can be used as input for the `loaddata` REST API.
Please note that when importing Linux/Unix users the input file is not required: `/etc/passwd` and `/etc/shadow` are automatically parsed. `/etc/shadow` read permission is typically granted to the `root` user only, so you need to execute `convertusers` as `root`.

View file

@ -0,0 +1,208 @@
#!/usr/bin/env python
import argparse
import json
import sys
import time
try:
import pwd
import spwd
except ImportError:
pwd = None
class ConvertUsers:
def __init__(self, input_file, users_format, output_file, min_uid, max_uid, usernames, force_uid, force_gid):
self.input_file = input_file
self.users_format = users_format
self.output_file = output_file
self.min_uid = min_uid
self.max_uid = max_uid
self.usernames = usernames
self.force_uid = force_uid
self.force_gid = force_gid
self.SFTPGoUsers = []
def buildUserObject(self, username, password, home_dir, uid, gid, max_sessions, quota_size, quota_files, upload_bandwidth,
download_bandwidth, status, expiration_date, allowed_ip=[], denied_ip=[]):
return {'id':0, 'username':username, 'password':password, 'home_dir':home_dir, 'uid':uid, 'gid':gid,
'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files, 'permissions':{'/':"*"},
'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth,
'status':status, 'expiration_date':expiration_date,
'filters':{'allowed_ip':allowed_ip, 'denied_ip':denied_ip}}
def addUser(self, user):
user['id'] = len(self.SFTPGoUsers) + 1
print('')
print('New user imported: {}'.format(user))
print('')
self.SFTPGoUsers.append(user)
def saveUsers(self):
if self.SFTPGoUsers:
data = {'users':self.SFTPGoUsers}
jsonData = json.dumps(data)
with open(self.output_file, 'w') as f:
f.write(jsonData)
print()
print('Number of users saved to "{}": {}. You can import them using loaddata'.format(self.output_file,
len(self.SFTPGoUsers)))
print()
sys.exit(0)
else:
print('No user imported')
sys.exit(1)
def convert(self):
if self.users_format == 'unix-passwd':
self.convertFromUnixPasswd()
elif self.users_format == 'pure-ftpd':
self.convertFromPureFTPD()
else:
self.convertFromProFTPD()
self.saveUsers()
def isUserValid(self, username, uid):
if self.usernames and not username in self.usernames:
return False
if self.min_uid >= 0 and uid < self.min_uid:
return False
if self.max_uid >= 0 and uid > self.max_uid:
return False
return True
def convertFromUnixPasswd(self):
days_from_epoch_time = time.time() / 86400
for user in pwd.getpwall():
username = user.pw_name
password = user.pw_passwd
uid = user.pw_uid
gid = user.pw_gid
home_dir = user.pw_dir
status = 1
expiration_date = 0
if not self.isUserValid(username, uid):
continue
if self.force_uid >= 0:
uid = self.force_uid
if self.force_gid >= 0:
gid = self.force_gid
# FIXME: if the passwords aren't in /etc/shadow they are probably DES encrypted and we don't support them
if password == 'x' or password == '*':
user_info = spwd.getspnam(username)
password = user_info.sp_pwdp
if not password or password == '!!' or password == '!*':
print('cannot import user "{}" without a password'.format(username))
continue
if user_info.sp_inact > 0:
last_pwd_change_diff = days_from_epoch_time - user_info.sp_lstchg
if last_pwd_change_diff > user_info.sp_inact:
status = 0
if user_info.sp_expire > 0:
expiration_date = user_info.sp_expire * 86400
self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, status,
expiration_date))
def convertFromProFTPD(self):
with open(self.input_file, 'r') as f:
for line in f:
fields = line.split(':')
if len(fields) > 6:
username = fields[0]
password = fields[1]
uid = int(fields[2])
gid = int(fields[3])
home_dir = fields[5]
if not self.isUserValid(username, uid):
continue
if self.force_uid >= 0:
uid = self.force_uid
if self.force_gid >= 0:
gid = self.force_gid
self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, 1, 0))
def convertPureFTPDIP(self, fields):
result = []
if not fields:
return result
for v in fields.split(','):
ip_mask = v.strip()
if not ip_mask:
continue
if ip_mask.count('.') < 3 and ip_mask.count(':') < 3:
print('cannot import pure-ftpd IP: {}'.format(ip_mask))
continue
if '/' not in ip_mask:
ip_mask += '/32'
result.append(ip_mask)
return result
def convertFromPureFTPD(self):
with open(self.input_file, 'r') as f:
for line in f:
fields = line.split(':')
if len(fields) > 16:
username = fields[0]
password = fields[1]
uid = int(fields[2])
gid = int(fields[3])
home_dir = fields[5]
upload_bandwidth = 0
if fields[6]:
upload_bandwidth = int(int(fields[6]) / 1024)
download_bandwidth = 0
if fields[7]:
download_bandwidth = int(int(fields[7]) / 1024)
max_sessions = 0
if fields[10]:
max_sessions = int(fields[10])
quota_files = 0
if fields[11]:
quota_files = int(fields[11])
quota_size = 0
if fields[12]:
quota_size = int(fields[12])
allowed_ip = self.convertPureFTPDIP(fields[15])
denied_ip = self.convertPureFTPDIP(fields[16])
if not self.isUserValid(username, uid):
continue
if self.force_uid >= 0:
uid = self.force_uid
if self.force_gid >= 0:
gid = self.force_gid
self.addUser(self.buildUserObject(username, password, home_dir, uid, gid, max_sessions, quota_size,
quota_files, upload_bandwidth, download_bandwidth, 1, 0, allowed_ip,
denied_ip))
if __name__ == '__main__':
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=
'Convert users to a JSON format suitable to use with loadddata')
supportedUsersFormats = []
help_text = ''
if pwd is not None:
supportedUsersFormats.append('unix-passwd')
help_text = 'To import from unix-passwd format you need the permission to read /etc/shadow that is typically granted to the root user only'
supportedUsersFormats.append('pure-ftpd')
supportedUsersFormats.append('proftpd')
parser.add_argument('input_file', type=str)
parser.add_argument('users_format', type=str, choices=supportedUsersFormats, help=help_text)
parser.add_argument('output_file', type=str)
parser.add_argument('--min-uid', type=int, default=-1, help='if >= 0 only import users with UID greater or equal ' +
'to this value. Default: %(default)s')
parser.add_argument('--max-uid', type=int, default=-1, help='if >= 0 only import users with UID lesser or equal ' +
'to this value. Default: %(default)s')
parser.add_argument('--usernames', type=str, nargs='+', default=[], help='Only import users with these usernames. ' +
'Default: %(default)s')
parser.add_argument('--force-uid', type=int, default=-1, help='if >= 0 the imported users will have this UID in ' +
'SFTPGo. Default: %(default)s')
parser.add_argument('--force-gid', type=int, default=-1, help='if >= 0 the imported users will have this GID in ' +
'SFTPGo. Default: %(default)s')
args = parser.parse_args()
convertUsers = ConvertUsers(args.input_file, args.users_format, args.output_file, args.min_uid, args.max_uid,
args.usernames, args.force_uid, args.force_gid)
convertUsers.convert()

View file

@ -1,5 +1,7 @@
# REST API CLI client
:warning: This sample client is deprecated and it will work only with api V1 (SFTPGo <= 1.2.2). You can easily build your own client from the OpenAPI schema.
`sftpgo_api_cli` is a very simple command line client for `SFTPGo` REST API written in python.
It has the following requirements:

View file

@ -14,14 +14,14 @@ import (
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/httpdtest"
"github.com/drakkan/sftpgo/kms"
)
func TestBasicFTPHandlingCryptFs(t *testing.T) {
u := getTestUserWithCryptFs()
u.QuotaSize = 6553600
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -56,7 +56,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
assert.Len(t, list, 1)
assert.Equal(t, testFileSize, int64(list[0].Size))
}
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
@ -66,7 +66,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
assert.Error(t, err)
err = client.Delete(testFileName + "1")
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)
@ -108,7 +108,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -117,7 +117,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
func TestZeroBytesTransfersCryptFs(t *testing.T) {
u := getTestUserWithCryptFs()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -146,7 +146,7 @@ func TestZeroBytesTransfersCryptFs(t *testing.T) {
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -154,7 +154,7 @@ func TestZeroBytesTransfersCryptFs(t *testing.T) {
func TestResumeCryptFs(t *testing.T) {
u := getTestUserWithCryptFs()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -207,7 +207,7 @@ func TestResumeCryptFs(t *testing.T) {
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)

View file

@ -28,7 +28,7 @@ import (
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/ftpd"
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/httpdtest"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
@ -132,7 +132,7 @@ func TestMain(m *testing.M) {
logger.WarnToConsole("error initializing common: %v", err)
os.Exit(1)
}
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
if err != nil {
logger.ErrorToConsole("error initializing data provider: %v", err)
os.Exit(1)
@ -150,7 +150,7 @@ func TestMain(m *testing.M) {
httpdConf := config.GetHTTPDConfig()
httpdConf.BindPort = 8079
httpd.SetBaseURLAndCredentials("http://127.0.0.1:8079", "", "")
httpdtest.SetBaseURL("http://127.0.0.1:8079")
ftpdConf := config.GetFTPDConfig()
ftpdConf.Bindings = []ftpd.Binding{
@ -298,11 +298,11 @@ func TestInitializationFailure(t *testing.T) {
func TestBasicFTPHandling(t *testing.T) {
u := getTestUser()
u.QuotaSize = 6553600
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.QuotaSize = 6553600
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
@ -332,7 +332,7 @@ func TestBasicFTPHandling(t *testing.T) {
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
@ -342,7 +342,7 @@ func TestBasicFTPHandling(t *testing.T) {
assert.Error(t, err)
err = client.Delete(testFileName + "1")
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
@ -385,9 +385,9 @@ func TestBasicFTPHandling(t *testing.T) {
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -396,12 +396,12 @@ func TestBasicFTPHandling(t *testing.T) {
func TestLoginInvalidPwd(t *testing.T) {
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
user.Password = "wrong"
_, err = getFTPClient(user, false)
assert.Error(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
@ -425,7 +425,7 @@ func TestLoginExternalAuth(t *testing.T) {
assert.NoError(t, err)
providerConf.ExternalAuthHook = extAuthPath
providerConf.ExternalAuthScope = 0
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
client, err := getFTPClient(u, true)
if assert.NoError(t, err) {
@ -441,22 +441,20 @@ func TestLoginExternalAuth(t *testing.T) {
assert.NoError(t, err)
}
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
if assert.Len(t, users, 1) {
user := users[0]
assert.Equal(t, defaultUsername, user.Username)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
assert.Equal(t, defaultUsername, user.Username)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
err = dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = os.Remove(extAuthPath)
assert.NoError(t, err)
@ -475,11 +473,10 @@ func TestPreLoginHook(t *testing.T) {
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)
assert.NoError(t, err)
providerConf.PreLoginHook = preLoginPath
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)
assert.NoError(t, err)
assert.Equal(t, 0, len(users))
client, err := getFTPClient(u, false)
if assert.NoError(t, err) {
err = checkBasicFTP(client)
@ -488,10 +485,8 @@ func TestPreLoginHook(t *testing.T) {
assert.NoError(t, err)
}
users, _, err = httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 1, len(users))
user := users[0]
// test login with an existing user
client, err = getFTPClient(user, true)
@ -518,7 +513,7 @@ func TestPreLoginHook(t *testing.T) {
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -527,7 +522,7 @@ func TestPreLoginHook(t *testing.T) {
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = os.Remove(preLoginPath)
assert.NoError(t, err)
@ -540,7 +535,7 @@ func TestPostConnectHook(t *testing.T) {
common.Config.PostConnectHook = postConnectPath
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
err = ioutil.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
assert.NoError(t, err)
@ -559,7 +554,7 @@ func TestPostConnectHook(t *testing.T) {
assert.NoError(t, err)
}
common.Config.PostConnectHook = "http://127.0.0.1:8079/api/v1/version"
common.Config.PostConnectHook = "http://127.0.0.1:8079/healthz"
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -577,7 +572,7 @@ func TestPostConnectHook(t *testing.T) {
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -589,7 +584,7 @@ func TestMaxConnections(t *testing.T) {
oldValue := common.Config.MaxTotalConnections
common.Config.MaxTotalConnections = 1
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -600,7 +595,7 @@ func TestMaxConnections(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -618,7 +613,7 @@ func TestDefender(t *testing.T) {
err := common.Initialize(cfg)
assert.NoError(t, err)
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, false)
if assert.NoError(t, err) {
@ -640,7 +635,7 @@ func TestDefender(t *testing.T) {
assert.Contains(t, err.Error(), "Access denied, banned client IP")
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -652,7 +647,7 @@ func TestDefender(t *testing.T) {
func TestMaxSessions(t *testing.T) {
u := getTestUser()
u.MaxSessions = 1
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -663,7 +658,7 @@ func TestMaxSessions(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -671,7 +666,7 @@ func TestMaxSessions(t *testing.T) {
func TestZeroBytesTransfers(t *testing.T) {
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, useTLS := range []bool{true, false} {
client, err := getFTPClient(user, useTLS)
@ -699,7 +694,7 @@ func TestZeroBytesTransfers(t *testing.T) {
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -727,7 +722,7 @@ func TestDownloadErrors(t *testing.T) {
DeniedPatterns: []string{"*.jpg"},
},
}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -758,7 +753,7 @@ func TestDownloadErrors(t *testing.T) {
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -779,7 +774,7 @@ func TestUploadErrors(t *testing.T) {
DeniedExtensions: []string{".zip"},
},
}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -823,7 +818,7 @@ func TestUploadErrors(t *testing.T) {
err = os.Remove(testFilePath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -831,9 +826,9 @@ func TestUploadErrors(t *testing.T) {
func TestResume(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client, err := getFTPClient(user, true)
@ -888,9 +883,9 @@ func TestResume(t *testing.T) {
}
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -900,12 +895,12 @@ func TestResume(t *testing.T) {
func TestDeniedLoginMethod(t *testing.T) {
u := getTestUser()
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
_, err = getFTPClient(user, false)
assert.Error(t, err)
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndPassword}
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -913,7 +908,7 @@ func TestDeniedLoginMethod(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -923,12 +918,12 @@ func TestDeniedLoginMethod(t *testing.T) {
func TestDeniedProtocols(t *testing.T) {
u := getTestUser()
u.Filters.DeniedProtocols = []string{common.ProtocolFTP}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
_, err = getFTPClient(user, false)
assert.Error(t, err)
user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolWebDAV}
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -936,7 +931,7 @@ func TestDeniedProtocols(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -945,11 +940,11 @@ func TestDeniedProtocols(t *testing.T) {
func TestQuotaLimits(t *testing.T) {
u := getTestUser()
u.QuotaFiles = 1
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.QuotaFiles = 1
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
testFileSize := int64(65535)
@ -981,7 +976,7 @@ func TestQuotaLimits(t *testing.T) {
// test quota size
user.QuotaSize = testFileSize - 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, true)
if assert.NoError(t, err) {
@ -995,7 +990,7 @@ func TestQuotaLimits(t *testing.T) {
// now test quota limits while uploading the current file, we have 1 bytes remaining
user.QuotaSize = testFileSize + 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1031,13 +1026,13 @@ func TestQuotaLimits(t *testing.T) {
assert.NoError(t, err)
user.QuotaFiles = 0
user.QuotaSize = 0
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1047,11 +1042,11 @@ func TestUploadMaxSize(t *testing.T) {
testFileSize := int64(65535)
u := getTestUser()
u.Filters.MaxUploadFileSize = testFileSize + 1
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.Filters.MaxUploadFileSize = testFileSize + 1
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
testFilePath := filepath.Join(homeBasePath, testFileName)
@ -1084,13 +1079,13 @@ func TestUploadMaxSize(t *testing.T) {
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
user.Filters.MaxUploadFileSize = 65536000
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1100,7 +1095,7 @@ func TestLoginWithIPilters(t *testing.T) {
u := getTestUser()
u.Filters.DeniedIP = []string{"192.167.0.0/24", "172.18.0.0/16"}
u.Filters.AllowedIP = []string{"172.19.0.0/16"}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if !assert.Error(t, err) {
@ -1108,7 +1103,7 @@ func TestLoginWithIPilters(t *testing.T) {
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1129,7 +1124,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoError(t, dataprovider.Close())
err := dataprovider.Initialize(providerConf, configDir)
err := dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
if _, err = os.Stat(credentialsFile); err == nil {
@ -1137,7 +1132,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoError(t, os.Remove(credentialsFile))
}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())
@ -1152,7 +1147,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1160,7 +1155,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoError(t, dataprovider.Close())
assert.NoError(t, config.LoadConfig(configDir, ""))
providerConf = config.GetProviderConf()
assert.NoError(t, dataprovider.Initialize(providerConf, configDir))
assert.NoError(t, dataprovider.Initialize(providerConf, configDir, true))
}
func TestLoginInvalidFs(t *testing.T) {
@ -1168,7 +1163,7 @@ func TestLoginInvalidFs(t *testing.T) {
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "test"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
providerConf := config.GetProviderConf()
@ -1186,7 +1181,7 @@ func TestLoginInvalidFs(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1194,7 +1189,7 @@ func TestLoginInvalidFs(t *testing.T) {
func TestClientClose(t *testing.T) {
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -1207,7 +1202,7 @@ func TestClientClose(t *testing.T) {
1*time.Second, 50*time.Millisecond)
}
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1215,9 +1210,9 @@ func TestClientClose(t *testing.T) {
func TestRename(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
testDir := "adir"
@ -1262,7 +1257,7 @@ func TestRename(t *testing.T) {
assert.NoError(t, err)
}
user.Permissions[path.Join("/", testDir)] = []string{dataprovider.PermListItems}
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1277,15 +1272,15 @@ func TestRename(t *testing.T) {
if user.Username == defaultUsername {
user.Permissions = make(map[string][]string)
user.Permissions["/"] = allPerms
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1293,9 +1288,9 @@ func TestRename(t *testing.T) {
func TestSymlink(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
@ -1342,9 +1337,9 @@ func TestSymlink(t *testing.T) {
err = os.Remove(testFilePath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1353,9 +1348,9 @@ func TestSymlink(t *testing.T) {
func TestStat(t *testing.T) {
u := getTestUser()
u.Permissions["/subdir"] = []string{dataprovider.PermUpload}
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
@ -1391,9 +1386,9 @@ func TestStat(t *testing.T) {
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1413,7 +1408,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
})
err := os.MkdirAll(mappedPath, os.ModePerm)
assert.NoError(t, err)
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1423,7 +1418,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
assert.NoError(t, err)
err = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0)
assert.NoError(t, err)
folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
assert.NoError(t, err)
if assert.Len(t, folder, 1) {
f := folder[0]
@ -1432,7 +1427,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
}
err = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0)
assert.NoError(t, err)
folder, _, err = httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
folder, _, err = httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
assert.NoError(t, err)
if assert.Len(t, folder, 1) {
f := folder[0]
@ -1444,9 +1439,9 @@ func TestUploadOverwriteVfolder(t *testing.T) {
err = os.Remove(testFilePath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1466,7 +1461,7 @@ func TestAllocateAvailable(t *testing.T) {
})
err := os.MkdirAll(mappedPath, os.ModePerm)
assert.NoError(t, err)
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1488,7 +1483,7 @@ func TestAllocateAvailable(t *testing.T) {
assert.NoError(t, err)
}
user.QuotaSize = 100
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1537,7 +1532,7 @@ func TestAllocateAvailable(t *testing.T) {
user.Filters.MaxUploadFileSize = 100
user.QuotaSize = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1560,7 +1555,7 @@ func TestAllocateAvailable(t *testing.T) {
}
user.QuotaSize = 50
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1572,7 +1567,7 @@ func TestAllocateAvailable(t *testing.T) {
user.QuotaSize = 1000
user.Filters.MaxUploadFileSize = 1
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getFTPClient(user, false)
if assert.NoError(t, err) {
@ -1582,9 +1577,9 @@ func TestAllocateAvailable(t *testing.T) {
assert.Equal(t, "1", response)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1594,9 +1589,9 @@ func TestAllocateAvailable(t *testing.T) {
func TestAvailableUnsupportedFs(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(sftpUser, false)
if assert.NoError(t, err) {
@ -1608,9 +1603,9 @@ func TestAvailableUnsupportedFs(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1618,9 +1613,9 @@ func TestAvailableUnsupportedFs(t *testing.T) {
func TestChtimes(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
@ -1651,9 +1646,9 @@ func TestChtimes(t *testing.T) {
}
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1663,7 +1658,7 @@ func TestChown(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("chown is not supported on Windows")
}
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true)
if assert.NoError(t, err) {
@ -1686,7 +1681,7 @@ func TestChown(t *testing.T) {
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1697,9 +1692,9 @@ func TestChmod(t *testing.T) {
t.Skip("chmod is partially supported on Windows")
}
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client, err := getFTPClient(user, true)
@ -1733,9 +1728,9 @@ func TestChmod(t *testing.T) {
}
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1743,9 +1738,9 @@ func TestChmod(t *testing.T) {
func TestCombineDisabled(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client, err := getFTPClient(user, true)
@ -1763,9 +1758,9 @@ func TestCombineDisabled(t *testing.T) {
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1773,7 +1768,7 @@ func TestCombineDisabled(t *testing.T) {
func TestActiveModeDisabled(t *testing.T) {
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClientImplicitTLS(user)
if assert.NoError(t, err) {
@ -1809,7 +1804,7 @@ func TestActiveModeDisabled(t *testing.T) {
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1817,7 +1812,7 @@ func TestActiveModeDisabled(t *testing.T) {
func TestSITEDisabled(t *testing.T) {
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClientImplicitTLS(user)
if assert.NoError(t, err) {
@ -1832,7 +1827,7 @@ func TestSITEDisabled(t *testing.T) {
err = client.Quit()
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1840,13 +1835,13 @@ func TestSITEDisabled(t *testing.T) {
func TestHASH(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
u = getTestUserWithCryptFs()
u.Username += "_crypt"
cryptUser, _, err := httpd.AddUser(u, http.StatusOK)
cryptUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser, cryptUser} {
client, err := getFTPClientImplicitTLS(user)
@ -1891,13 +1886,13 @@ func TestHASH(t *testing.T) {
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
_, err = httpd.RemoveUser(cryptUser, http.StatusOK)
_, err = httpdtest.RemoveUser(cryptUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(cryptUser.GetHomeDir())
assert.NoError(t, err)
@ -1905,9 +1900,9 @@ func TestHASH(t *testing.T) {
func TestCombine(t *testing.T) {
u := getTestUser()
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client, err := getFTPClientImplicitTLS(user)
@ -1945,9 +1940,9 @@ func TestCombine(t *testing.T) {
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)

21
go.mod
View file

@ -3,34 +3,39 @@ module github.com/drakkan/sftpgo
go 1.15
require (
cloud.google.com/go v0.74.0 // indirect
cloud.google.com/go v0.75.0 // indirect
cloud.google.com/go/storage v1.12.0
github.com/Azure/azure-storage-blob-go v0.12.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b
github.com/aws/aws-sdk-go v1.36.20
github.com/aws/aws-sdk-go v1.36.28
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d
github.com/fclairamb/ftpserverlib v0.12.0
github.com/frankban/quicktest v1.11.2 // indirect
github.com/go-chi/chi v1.5.1
github.com/go-chi/jwtauth v1.1.1
github.com/go-chi/render v1.0.1
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.1.4 // indirect
github.com/google/uuid v1.1.5 // indirect
github.com/grandcat/zeroconf v1.0.0
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/lestrrat-go/jwx v1.0.8
github.com/lib/pq v1.9.0
github.com/magiconair/properties v1.8.4 // indirect
github.com/mattn/go-sqlite3 v1.14.6
github.com/miekg/dns v1.1.35 // indirect
github.com/minio/sha256-simd v0.1.1
github.com/minio/sio v0.2.1
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/otiai10/copy v1.4.2
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/pires/go-proxyproto v0.3.3
github.com/pkg/sftp v1.12.1-0.20201128220914-b5b6f3393fe9
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/procfs v0.3.0 // indirect
github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b
github.com/rs/xid v1.2.1
github.com/rs/zerolog v1.20.0
@ -49,12 +54,16 @@ require (
gocloud.dev v0.21.0
gocloud.dev/secrets/hashivault v0.21.0
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/mod v0.4.1 // indirect
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 // indirect
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78
golang.org/x/text v0.3.5 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee // indirect
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 // indirect
google.golang.org/api v0.36.0
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d // indirect
google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f // indirect
google.golang.org/grpc v1.35.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0 // indirect

64
go.sum
View file

@ -17,8 +17,8 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0 h1:kpgPA77kSSbjSs+fWHkPTxQ6J5Z2Qkruo5jfXEkHxNQ=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -106,8 +106,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.36.20 h1:IQr81xegCd40Xq21ZjFToKw9llaCzO1LRE75CgnvJ1Q=
github.com/aws/aws-sdk-go v1.36.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.36.28 h1:JVRN7BZgwQ31SQCBwG5QM445+ynJU0ruKu+miFIijYY=
github.com/aws/aws-sdk-go v1.36.28/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -131,6 +131,7 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@ -176,6 +177,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
@ -196,6 +198,8 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
github.com/go-chi/jwtauth v1.1.1 h1:CtUHwzvXUfZeZSbASLgzaTZQ8mL7p+vitX59NBTL1vY=
github.com/go-chi/jwtauth v1.1.1/go.mod h1:znOWz9e5/GfBOKiZlOUoEfjSjUF+cLZO3GcpkoGXvFI=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -210,6 +214,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@ -290,7 +295,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@ -298,8 +303,8 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0=
github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
@ -410,6 +415,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lestrrat-go/backoff/v2 v2.0.3 h1:2ABaTa5ifB1L90aoRMjaPa97p0WzzVe93Vggv8oZftw=
github.com/lestrrat-go/backoff/v2 v2.0.3/go.mod h1:mU93bMXuG27/Y5erI5E9weqavpTX5qiVFZI4uXAX0xk=
github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3 h1:e52qvXxpJPV/Kb2ovtuYgcRFjNmf9ntcn8BPIbpRM4k=
github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY=
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.0.6-0.20201127121120-26218808f029/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0=
github.com/lestrrat-go/jwx v1.0.8 h1:Mj/2Ey9rkGx4w5IMQ2Q+9KLZn4cZoMgKrnMxi9eXE3k=
github.com/lestrrat-go/jwx v1.0.8/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214zwZDdvho=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8=
github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
@ -450,8 +468,9 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks=
github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -540,8 +559,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.3.0 h1:Uehi/mxLK0eiUc0H0++5tpMGTexB8wZ598MIgU8VpDM=
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@ -682,6 +702,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -690,8 +711,9 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201203001011-0b49973bad19/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 h1:BaN3BAqnopnKjvl+15DYP6LLrbBHfbfmlFYzmFj/Q9Q=
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -722,6 +744,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -754,8 +777,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad h1:MCsdmFSdEd4UEa5TKS5JztCRHK/WtvNei1edOj5RSRo=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -764,8 +787,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -817,6 +841,7 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -832,8 +857,8 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -911,9 +936,9 @@ google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d h1:HV9Z9qMhQEsdlvxNFELgQ11RkMzO3CMkjEySjCtuLes=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f h1:izedQ6yVIc5mZsRuXzmSreCOlzI0lCU1HpG8yEdMiKw=
google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -936,8 +961,9 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

211
httpd/api_admin.go Normal file
View file

@ -0,0 +1,211 @@
package httpd
import (
"context"
"errors"
"net/http"
"strconv"
"github.com/go-chi/jwtauth"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/dataprovider"
)
type adminPwd struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
func getAdmins(w http.ResponseWriter, r *http.Request) {
limit := 100
offset := 0
order := dataprovider.OrderASC
var err error
if _, ok := r.URL.Query()["limit"]; ok {
limit, err = strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
err = errors.New("Invalid limit")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
if limit > 500 {
limit = 500
}
}
if _, ok := r.URL.Query()["offset"]; ok {
offset, err = strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
err = errors.New("Invalid offset")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
}
if _, ok := r.URL.Query()["order"]; ok {
order = r.URL.Query().Get("order")
if order != dataprovider.OrderASC && order != dataprovider.OrderDESC {
err = errors.New("Invalid order")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
}
admins, err := dataprovider.GetAdmins(limit, offset, order)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
render.JSON(w, r, admins)
}
func getAdminByUsername(w http.ResponseWriter, r *http.Request) {
username := getURLParam(r, "username")
renderAdmin(w, r, username, http.StatusOK)
}
func renderAdmin(w http.ResponseWriter, r *http.Request, username string, status int) {
admin, err := dataprovider.AdminExists(username)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
admin.HideConfidentialData()
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)
render.JSON(w, r.WithContext(ctx), admin)
} else {
render.JSON(w, r, admin)
}
}
func addAdmin(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var admin dataprovider.Admin
err := render.DecodeJSON(r.Body, &admin)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
err = dataprovider.AddAdmin(&admin)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
renderAdmin(w, r, admin.Username, http.StatusCreated)
}
func updateAdmin(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
username := getURLParam(r, "username")
admin, err := dataprovider.AdminExists(username)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
adminID := admin.ID
err = render.DecodeJSON(r.Body, &admin)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
if username == claims.Username {
if claims.isCriticalPermRemoved(admin.Permissions) {
sendAPIResponse(w, r, errors.New("You cannot remove these permissions to yourself"), "", http.StatusBadRequest)
return
}
if admin.Status == 0 {
sendAPIResponse(w, r, errors.New("You cannot disable yourself"), "", http.StatusBadRequest)
return
}
}
admin.ID = adminID
admin.Username = username
if err := dataprovider.UpdateAdmin(&admin); err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, nil, "Update admin", http.StatusOK)
}
func deleteAdmin(w http.ResponseWriter, r *http.Request) {
username := getURLParam(r, "username")
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
if username == claims.Username {
sendAPIResponse(w, r, errors.New("You cannot delete yourself"), "", http.StatusBadRequest)
return
}
err = dataprovider.DeleteAdmin(username)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, err, "Admin deleted", http.StatusOK)
}
func changeAdminPassword(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var pwd adminPwd
err := render.DecodeJSON(r.Body, &pwd)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
err = doChangeAdminPassword(r, pwd.CurrentPassword, pwd.NewPassword, pwd.NewPassword)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, err, "Password updated", http.StatusOK)
}
func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error {
if currentPassword == "" || newPassword == "" || confirmNewPassword == "" {
return dataprovider.NewValidationError("Please provide the current password and the new one two times")
}
if newPassword != confirmNewPassword {
return dataprovider.NewValidationError("The two password fields do not match")
}
if currentPassword == newPassword {
return dataprovider.NewValidationError("The new password must be different from the current one")
}
claims, err := getTokenClaims(r)
if err != nil {
return err
}
admin, err := dataprovider.AdminExists(claims.Username)
if err != nil {
return err
}
match, err := admin.CheckPassword(currentPassword)
if !match || err != nil {
return dataprovider.NewValidationError("Current password does not match")
}
admin.Password = newPassword
return dataprovider.UpdateAdmin(&admin)
}
func getTokenClaims(r *http.Request) (jwtTokenClaims, error) {
tokenClaims := jwtTokenClaims{}
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
return tokenClaims, err
}
tokenClaims.Decode(claims)
return tokenClaims, nil
}

View file

@ -1,6 +1,7 @@
package httpd
import (
"context"
"errors"
"net/http"
"strconv"
@ -64,16 +65,21 @@ func addFolder(w http.ResponseWriter, r *http.Request) {
return
}
err = dataprovider.AddFolder(&folder)
if err == nil {
folder, err = dataprovider.GetFolderByPath(folder.MappedPath)
if err == nil {
render.JSON(w, r, folder)
} else {
sendAPIResponse(w, r, err, "", getRespStatus(err))
}
} else {
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
renderFolder(w, r, folder.MappedPath)
}
func renderFolder(w http.ResponseWriter, r *http.Request, mappedPath string) {
folder, err := dataprovider.GetFolderByPath(mappedPath)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)
render.JSON(w, r.WithContext(ctx), folder)
}
func deleteFolderByPath(w http.ResponseWriter, r *http.Request) {
@ -87,15 +93,10 @@ func deleteFolderByPath(w http.ResponseWriter, r *http.Request) {
return
}
folder, err := dataprovider.GetFolderByPath(folderPath)
err := dataprovider.DeleteFolder(folderPath)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
err = dataprovider.DeleteFolder(&folder)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "Folder deleted", http.StatusOK)
}
sendAPIResponse(w, r, err, "Folder deleted", http.StatusOK)
}

View file

@ -112,7 +112,13 @@ func loadData(w http.ResponseWriter, r *http.Request) {
return
}
logger.Debug(logSender, "", "backup restored, users: %v", len(dump.Users))
if err = RestoreAdmins(dump.Admins, inputFile, mode); err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
logger.Debug(logSender, "", "backup restored, users: %v, folders: %v, admins: %vs",
len(dump.Users), len(dump.Folders), len(dump.Admins))
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
}
@ -164,6 +170,33 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, scanQuota
return nil
}
// RestoreAdmins restores the specified admins
func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) error {
for _, admin := range admins {
admin := admin // pin
a, err := dataprovider.AdminExists(admin.Username)
if err == nil {
if mode == 1 {
logger.Debug(logSender, "", "loaddata mode 1, existing admin %#v not updated", a.Username)
continue
}
admin.ID = a.ID
err = dataprovider.UpdateAdmin(&admin)
admin.Password = redactedSecret
logger.Debug(logSender, "", "restoring existing admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
} else {
err = dataprovider.AddAdmin(&admin)
admin.Password = redactedSecret
logger.Debug(logSender, "", "adding new admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
}
if err != nil {
return err
}
}
return nil
}
// RestoreUsers restores the specified users
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int) error {
for _, user := range users {
@ -176,14 +209,14 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
}
user.ID = u.ID
err = dataprovider.UpdateUser(&user)
user.Password = "[redacted]"
user.Password = redactedSecret
logger.Debug(logSender, "", "restoring existing user: %+v, dump file: %#v, error: %v", user, inputFile, err)
if mode == 2 && err == nil {
disconnectUser(user.Username)
}
} else {
err = dataprovider.AddUser(&user)
user.Password = "[redacted]"
user.Password = redactedSecret
logger.Debug(logSender, "", "adding new user: %+v, dump file: %#v, error: %v", user, inputFile, err)
}
if err != nil {

View file

@ -1,12 +1,12 @@
package httpd
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/common"
@ -16,11 +16,11 @@ import (
)
func getUsers(w http.ResponseWriter, r *http.Request) {
var err error
limit := 100
offset := 0
order := dataprovider.OrderASC
username := ""
var err error
if _, ok := r.URL.Query()["limit"]; ok {
limit, err = strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
@ -48,10 +48,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
return
}
}
if _, ok := r.URL.Query()["username"]; ok {
username = r.URL.Query().Get("username")
}
users, err := dataprovider.GetUsers(limit, offset, order, username)
users, err := dataprovider.GetUsers(limit, offset, order)
if err == nil {
render.JSON(w, r, users)
} else {
@ -59,19 +56,23 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
}
}
func getUserByID(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
func getUserByUsername(w http.ResponseWriter, r *http.Request) {
username := getURLParam(r, "username")
renderUser(w, r, username, http.StatusOK)
}
func renderUser(w http.ResponseWriter, r *http.Request, username string, status int) {
user, err := dataprovider.UserExists(username)
if err != nil {
err = errors.New("Invalid userID")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
user, err := dataprovider.GetUserByID(userID)
if err == nil {
user.HideConfidentialData()
render.JSON(w, r, user)
user.HideConfidentialData()
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)
render.JSON(w, r.WithContext(ctx), user)
} else {
sendAPIResponse(w, r, err, "", getRespStatus(err))
render.JSON(w, r, user)
}
}
@ -116,27 +117,18 @@ func addUser(w http.ResponseWriter, r *http.Request) {
}
}
err = dataprovider.AddUser(&user)
if err == nil {
user, err = dataprovider.UserExists(user.Username)
if err == nil {
user.HideConfidentialData()
render.JSON(w, r, user)
} else {
sendAPIResponse(w, r, err, "", getRespStatus(err))
}
} else {
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
renderUser(w, r, user.Username, http.StatusCreated)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
if err != nil {
err = errors.New("Invalid userID")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
var err error
username := getURLParam(r, "username")
disconnect := 0
if _, ok := r.URL.Query()["disconnect"]; ok {
disconnect, err = strconv.Atoi(r.URL.Query().Get("disconnect"))
@ -146,11 +138,12 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
return
}
}
user, err := dataprovider.GetUserByID(userID)
user, err := dataprovider.UserExists(username)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
userID := user.ID
currentPermissions := user.Permissions
currentS3AccessSecret := user.FsConfig.S3Config.AccessSecret
currentAzAccountKey := user.FsConfig.AzBlobConfig.AccountKey
@ -170,6 +163,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
user.ID = userID
user.Username = username
user.SetEmptySecretsIfNil()
// we use new Permissions if passed otherwise the old ones
if len(user.Permissions) == 0 {
@ -177,40 +172,26 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
}
updateEncryptedSecrets(&user, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials, currentCryptoPassphrase,
currentSFTPPassword, currentSFTPKey)
if user.ID != userID {
sendAPIResponse(w, r, err, "user ID in request body does not match user ID in path parameter", http.StatusBadRequest)
return
}
err = dataprovider.UpdateUser(&user)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
} else {
sendAPIResponse(w, r, err, "User updated", http.StatusOK)
if disconnect == 1 {
disconnectUser(user.Username)
}
return
}
sendAPIResponse(w, r, err, "User updated", http.StatusOK)
if disconnect == 1 {
disconnectUser(user.Username)
}
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
if err != nil {
err = errors.New("Invalid userID")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
user, err := dataprovider.GetUserByID(userID)
username := getURLParam(r, "username")
err := dataprovider.DeleteUser(username)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
err = dataprovider.DeleteUser(&user)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "User deleted", http.StatusOK)
disconnectUser(user.Username)
}
sendAPIResponse(w, r, err, "User deleted", http.StatusOK)
disconnectUser(username)
}
func disconnectUser(username string) {

File diff suppressed because it is too large Load diff

View file

@ -1,34 +0,0 @@
package httpd
import (
"net/http"
"strings"
"github.com/drakkan/sftpgo/common"
)
func checkAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !validateCredentials(r) {
w.Header().Set(common.HTTPAuthenticationHeader, "Basic realm=\"SFTPGo Web\"")
if strings.HasPrefix(r.RequestURI, apiPrefix) {
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
} else {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
return
}
next.ServeHTTP(w, r)
})
}
func validateCredentials(r *http.Request) bool {
if !httpAuth.IsEnabled() {
return true
}
username, password, ok := r.BasicAuth()
if !ok {
return false
}
return httpAuth.ValidateCredentials(username, password)
}

147
httpd/auth_utils.go Normal file
View file

@ -0,0 +1,147 @@
package httpd
import (
"net/http"
"time"
"github.com/go-chi/jwtauth"
"github.com/lestrrat-go/jwx/jwt"
"github.com/rs/xid"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/utils"
)
const (
claimUsernameKey = "username"
claimPermissionsKey = "permissions"
basicRealm = "Basic realm=\"SFTPGo\""
)
var (
tokenDuration = 10 * time.Minute
tokenRefreshMin = 5 * time.Minute
)
type jwtTokenClaims struct {
Username string
Permissions []string
Signature string
}
func (c *jwtTokenClaims) asMap() map[string]interface{} {
claims := make(map[string]interface{})
claims[claimUsernameKey] = c.Username
claims[claimPermissionsKey] = c.Permissions
claims[jwt.SubjectKey] = c.Signature
return claims
}
func (c *jwtTokenClaims) Decode(token map[string]interface{}) {
username := token[claimUsernameKey]
switch v := username.(type) {
case string:
c.Username = v
}
signature := token[jwt.SubjectKey]
switch v := signature.(type) {
case string:
c.Signature = v
}
permissions := token[claimPermissionsKey]
switch v := permissions.(type) {
case []interface{}:
for _, elem := range v {
switch elemValue := elem.(type) {
case string:
c.Permissions = append(c.Permissions, elemValue)
}
}
}
}
func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool {
if utils.IsStringInSlice(dataprovider.PermAdminAny, permissions) {
return false
}
if (utils.IsStringInSlice(dataprovider.PermAdminManageAdmins, c.Permissions) ||
utils.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions)) &&
!utils.IsStringInSlice(dataprovider.PermAdminManageAdmins, permissions) &&
!utils.IsStringInSlice(dataprovider.PermAdminAny, permissions) {
return true
}
return false
}
func (c *jwtTokenClaims) hasPerm(perm string) bool {
if utils.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions) {
return true
}
return utils.IsStringInSlice(perm, c.Permissions)
}
func (c *jwtTokenClaims) createTokenResponse(tokenAuth *jwtauth.JWTAuth) (map[string]interface{}, error) {
claims := c.asMap()
now := time.Now().UTC()
claims[jwt.JwtIDKey] = xid.New().String()
claims[jwt.NotBeforeKey] = now.Add(-30 * time.Second)
claims[jwt.ExpirationKey] = now.Add(tokenDuration)
token, tokenString, err := tokenAuth.Encode(claims)
if err != nil {
return nil, err
}
response := make(map[string]interface{})
response["access_token"] = tokenString
response["expires_at"] = token.Expiration().Format(time.RFC3339)
return response, nil
}
func (c *jwtTokenClaims) createAndSetCookie(w http.ResponseWriter, tokenAuth *jwtauth.JWTAuth) error {
resp, err := c.createTokenResponse(tokenAuth)
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: resp["access_token"].(string),
Path: webBasePath,
Expires: time.Now().Add(tokenDuration),
HttpOnly: true,
})
return nil
}
func (c *jwtTokenClaims) removeCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: "",
Path: webBasePath,
MaxAge: -1,
HttpOnly: true,
})
}
func getAdminFromToken(r *http.Request) *dataprovider.Admin {
admin := &dataprovider.Admin{}
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
return admin
}
tokenClaims := jwtTokenClaims{}
tokenClaims.Decode(claims)
admin.Username = tokenClaims.Username
admin.Permissions = tokenClaims.Permissions
return admin
}

View file

@ -1,19 +1,16 @@
// Package httpd implements REST API and Web interface for SFTPGo.
// REST API allows to manage users and quota and to get real time reports for the active connections
// with possibility of forcibly closing a connection.
// The OpenAPI 3 schema for the exposed API can be found inside the source tree:
// https://github.com/drakkan/sftpgo/blob/master/httpd/schema/openapi.yaml
// A basic Web interface to manage users and connections is provided too
package httpd
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"net/url"
"path/filepath"
"runtime"
"time"
"strings"
"github.com/go-chi/chi"
@ -28,29 +25,38 @@ import (
const (
logSender = "httpd"
apiPrefix = "/api/v1"
activeConnectionsPath = "/api/v1/connection"
quotaScanPath = "/api/v1/quota_scan"
quotaScanVFolderPath = "/api/v1/folder_quota_scan"
userPath = "/api/v1/user"
versionPath = "/api/v1/version"
folderPath = "/api/v1/folder"
serverStatusPath = "/api/v1/status"
dumpDataPath = "/api/v1/dumpdata"
loadDataPath = "/api/v1/loaddata"
updateUsedQuotaPath = "/api/v1/quota_update"
updateFolderUsedQuotaPath = "/api/v1/folder_quota_update"
defenderBanTime = "/api/v1/defender/ban_time"
defenderUnban = "/api/v1/defender/unban"
defenderScore = "/api/v1/defender/score"
metricsPath = "/metrics"
tokenPath = "/api/v2/token"
activeConnectionsPath = "/api/v2/connections"
quotaScanPath = "/api/v2/quota-scans"
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
userPath = "/api/v2/users"
versionPath = "/api/v2/version"
folderPath = "/api/v2/folders"
serverStatusPath = "/api/v2/status"
dumpDataPath = "/api/v2/dumpdata"
loadDataPath = "/api/v2/loaddata"
updateUsedQuotaPath = "/api/v2/quota-update"
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
defenderBanTime = "/api/v2/defender/bantime"
defenderUnban = "/api/v2/defender/unban"
defenderScore = "/api/v2/defender/score"
adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/changepwd/admin"
healthzPath = "/healthz"
webBasePath = "/web"
webLoginPath = "/web/login"
webLogoutPath = "/web/logout"
webUsersPath = "/web/users"
webUserPath = "/web/user"
webConnectionsPath = "/web/connections"
webFoldersPath = "/web/folders"
webFolderPath = "/web/folder"
webStatusPath = "/web/status"
webAdminsPath = "/web/admins"
webAdminPath = "/web/admin"
webScanVFolderPath = "/web/folder-quota-scans"
webQuotaScanPath = "/web/quota-scans"
webChangeAdminPwdPath = "/web/changepwd/admin"
webStaticFilesPath = "/static"
// MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB
@ -59,12 +65,18 @@ const (
)
var (
router *chi.Mux
backupsPath string
httpAuth common.HTTPAuthProvider
certMgr *common.CertManager
)
// Binding defines the configuration for a network listener
type Binding struct {
// The address to listen on. A blank value means listen on all available network interfaces.
Address string `json:"address" mapstructure:"address"`
// The port used for serving requests
Port int `json:"port" mapstructure:"port"`
}
type defenderStatus struct {
IsActive bool `json:"is_active"`
}
@ -91,12 +103,6 @@ type Conf struct {
StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
// Path to the backup directory. This can be an absolute path or a path relative to the config dir
BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
// Path to a file used to store usernames and password for basic authentication.
// This can be an absolute path or a path relative to the config dir.
// We support HTTP basic authentication and the file format must conform to the one generated using the Apache
// htpasswd tool. The supported password formats are bcrypt ($2y$ prefix) and md5 crypt ($apr1$ prefix).
// If empty HTTP authentication is disabled
AuthUserFile string `json:"auth_user_file" mapstructure:"auth_user_file"`
// If files containing a certificate and matching private key for the server are provided the server will expect
// HTTPS connections.
// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
@ -128,7 +134,7 @@ func (c Conf) Initialize(configDir string) error {
backupsPath = getConfigPath(c.BackupsPath, configDir)
staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
templatesPath := getConfigPath(c.TemplatesPath, configDir)
enableWebAdmin := len(staticFilesPath) > 0 || len(templatesPath) > 0
enableWebAdmin := staticFilesPath != "" || templatesPath != ""
if backupsPath == "" {
return fmt.Errorf("Required directory is invalid, backup path %#v", backupsPath)
}
@ -136,11 +142,6 @@ func (c Conf) Initialize(configDir string) error {
return fmt.Errorf("Required directory is invalid, static file path: %#v template path: %#v",
staticFilesPath, templatesPath)
}
authUserFile := getConfigPath(c.AuthUserFile, configDir)
httpAuth, err = common.NewBasicAuthProvider(authUserFile)
if err != nil {
return err
}
certificateFile := getConfigPath(c.CertificateFile, configDir)
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
if enableWebAdmin {
@ -148,28 +149,18 @@ func (c Conf) Initialize(configDir string) error {
} else {
logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it")
}
initializeRouter(staticFilesPath, enableWebAdmin)
httpServer := &http.Server{
Handler: router,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 16, // 64KB
ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
}
if certificateFile != "" && certificateKeyFile != "" {
certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
if err != nil {
return err
}
config := &tls.Config{
GetCertificate: certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12,
}
httpServer.TLSConfig = config
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, logSender)
}
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, logSender)
server := newHttpdServer(c.BindAddress, c.BindPort, staticFilesPath, enableWebAdmin)
return server.listenAndServe()
}
func isWebAdminRequest(r *http.Request) bool {
return strings.HasPrefix(r.RequestURI, webBasePath+"/")
}
// ReloadCertificateMgr reloads the certificate manager
@ -202,3 +193,34 @@ func getServicesStatus() ServicesStatus {
}
return status
}
func getURLParam(r *http.Request, key string) string {
v := chi.URLParam(r, key)
unescaped, err := url.PathUnescape(v)
if err != nil {
return v
}
return unescaped
}
func fileServer(r chi.Router, path string, root http.FileSystem) {
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
path += "/"
}
path += "*"
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}
// GetHTTPRouter returns an HTTP handler suitable to use for test cases
func GetHTTPRouter() http.Handler {
server := newHttpdServer("", 8080, "../static", true)
server.initializeRouter()
return server.router
}

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,31 @@
package httpd
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"path"
"runtime"
"strings"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/jwtauth"
"github.com/lestrrat-go/jwx/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/vfs"
)
const (
invalidURL = "http://foo\x7f.com/"
inactiveURL = "http://127.0.0.1:12345"
)
func TestShouldBind(t *testing.T) {
@ -55,455 +53,6 @@ func TestGetRespStatus(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, respStatus)
}
func TestCheckResponse(t *testing.T) {
err := checkResponse(http.StatusOK, http.StatusCreated)
assert.Error(t, err)
err = checkResponse(http.StatusBadRequest, http.StatusBadRequest)
assert.NoError(t, err)
}
func TestCheckFolder(t *testing.T) {
expected := &vfs.BaseVirtualFolder{}
actual := &vfs.BaseVirtualFolder{}
err := checkFolder(expected, actual)
assert.Error(t, err)
expected.ID = 1
actual.ID = 2
err = checkFolder(expected, actual)
assert.Error(t, err)
expected.ID = 2
actual.ID = 2
expected.MappedPath = "path"
err = checkFolder(expected, actual)
assert.Error(t, err)
expected.MappedPath = ""
expected.LastQuotaUpdate = 1
err = checkFolder(expected, actual)
assert.Error(t, err)
expected.LastQuotaUpdate = 0
expected.UsedQuotaFiles = 1
err = checkFolder(expected, actual)
assert.Error(t, err)
expected.UsedQuotaFiles = 0
expected.UsedQuotaSize = 1
err = checkFolder(expected, actual)
assert.Error(t, err)
expected.UsedQuotaSize = 0
expected.Users = append(expected.Users, "user1")
err = checkFolder(expected, actual)
assert.Error(t, err)
actual.Users = append(actual.Users, "user2")
err = checkFolder(expected, actual)
assert.Error(t, err)
expected.Users = nil
actual.Users = nil
}
func TestCheckUser(t *testing.T) {
expected := &dataprovider.User{}
actual := &dataprovider.User{}
actual.Password = "password"
err := checkUser(expected, actual)
assert.Error(t, err)
actual.Password = ""
err = checkUser(expected, actual)
assert.Error(t, err)
expected.ID = 1
actual.ID = 2
err = checkUser(expected, actual)
assert.Error(t, err)
expected.ID = 2
actual.ID = 2
expected.Permissions = make(map[string][]string)
expected.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
actual.Permissions = make(map[string][]string)
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Permissions["/"] = append(expected.Permissions["/"], dataprovider.PermRename)
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Permissions = make(map[string][]string)
expected.Permissions["/somedir"] = []string{dataprovider.PermAny}
actual.Permissions = make(map[string][]string)
actual.Permissions["/otherdir"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Permissions = make(map[string][]string)
actual.Permissions = make(map[string][]string)
actual.FsConfig.Provider = dataprovider.S3FilesystemProvider
err = checkUser(expected, actual)
assert.Error(t, err)
actual.FsConfig.Provider = dataprovider.LocalFilesystemProvider
expected.VirtualFolders = append(expected.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
MappedPath: os.TempDir(),
},
VirtualPath: "/vdir",
})
err = checkUser(expected, actual)
assert.Error(t, err)
actual.VirtualFolders = append(actual.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
MappedPath: os.TempDir(),
},
VirtualPath: "/vdir1",
})
err = checkUser(expected, actual)
assert.Error(t, err)
}
func TestCompareUserFilters(t *testing.T) {
expected := &dataprovider.User{}
actual := &dataprovider.User{}
actual.ID = 1
expected.ID = 1
expected.Filters.AllowedIP = []string{}
actual.Filters.AllowedIP = []string{"192.168.1.2/32"}
err := checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.AllowedIP = []string{"192.168.1.3/32"}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.AllowedIP = []string{}
actual.Filters.AllowedIP = []string{}
expected.Filters.DeniedIP = []string{}
actual.Filters.DeniedIP = []string{"192.168.1.2/32"}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.DeniedIP = []string{"192.168.1.3/32"}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.DeniedIP = []string{}
actual.Filters.DeniedIP = []string{}
expected.Filters.DeniedLoginMethods = []string{}
actual.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.DeniedLoginMethods = []string{}
actual.Filters.DeniedLoginMethods = []string{}
actual.Filters.DeniedProtocols = []string{common.ProtocolFTP}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}
err = checkUser(expected, actual)
assert.Error(t, err)
expected.Filters.DeniedProtocols = []string{}
actual.Filters.DeniedProtocols = []string{}
expected.Filters.MaxUploadFileSize = 0
actual.Filters.MaxUploadFileSize = 100
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.MaxUploadFileSize = 0
expected.Filters.FileExtensions = append(expected.Filters.FileExtensions, dataprovider.ExtensionsFilter{
Path: "/",
AllowedExtensions: []string{".jpg", ".png"},
DeniedExtensions: []string{".zip", ".rar"},
})
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FileExtensions = append(actual.Filters.FileExtensions, dataprovider.ExtensionsFilter{
Path: "/sub",
AllowedExtensions: []string{".jpg", ".png"},
DeniedExtensions: []string{".zip", ".rar"},
})
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{
Path: "/",
AllowedExtensions: []string{".jpg"},
DeniedExtensions: []string{".zip", ".rar"},
}
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{
Path: "/",
AllowedExtensions: []string{".tiff", ".png"},
DeniedExtensions: []string{".zip", ".rar"},
}
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{
Path: "/",
AllowedExtensions: []string{".jpg", ".png"},
DeniedExtensions: []string{".tar.gz", ".rar"},
}
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FileExtensions = nil
actual.Filters.FilePatterns = nil
expected.Filters.FileExtensions = nil
expected.Filters.FilePatterns = nil
expected.Filters.FilePatterns = append(expected.Filters.FilePatterns, dataprovider.PatternsFilter{
Path: "/",
AllowedPatterns: []string{"*.jpg", "*.png"},
DeniedPatterns: []string{"*.zip", "*.rar"},
})
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FilePatterns = append(actual.Filters.FilePatterns, dataprovider.PatternsFilter{
Path: "/sub",
AllowedPatterns: []string{"*.jpg", "*.png"},
DeniedPatterns: []string{"*.zip", "*.rar"},
})
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{
Path: "/",
AllowedPatterns: []string{"*.jpg"},
DeniedPatterns: []string{"*.zip", "*.rar"},
}
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{
Path: "/",
AllowedPatterns: []string{"*.tiff", "*.png"},
DeniedPatterns: []string{"*.zip", "*.rar"},
}
err = checkUser(expected, actual)
assert.Error(t, err)
actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{
Path: "/",
AllowedPatterns: []string{"*.jpg", "*.png"},
DeniedPatterns: []string{"*.tar.gz", "*.rar"},
}
err = checkUser(expected, actual)
assert.Error(t, err)
}
func TestCompareUserFields(t *testing.T) {
expected := &dataprovider.User{}
actual := &dataprovider.User{}
expected.Permissions = make(map[string][]string)
actual.Permissions = make(map[string][]string)
expected.Username = "test"
err := compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.Username = ""
expected.HomeDir = "homedir"
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.HomeDir = ""
expected.UID = 1
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.UID = 0
expected.GID = 1
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.GID = 0
expected.MaxSessions = 2
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.MaxSessions = 0
expected.QuotaSize = 4096
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.QuotaSize = 0
expected.QuotaFiles = 2
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.QuotaFiles = 0
expected.Permissions["/"] = []string{dataprovider.PermCreateDirs}
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.Permissions = nil
expected.UploadBandwidth = 64
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.UploadBandwidth = 0
expected.DownloadBandwidth = 128
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.DownloadBandwidth = 0
expected.Status = 1
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.Status = 0
expected.ExpirationDate = 123
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
expected.ExpirationDate = 0
expected.AdditionalInfo = "info"
err = compareEqualsUserFields(expected, actual)
assert.Error(t, err)
}
func TestCompareUserFsConfig(t *testing.T) {
secretString := "access secret"
expected := &dataprovider.User{}
actual := &dataprovider.User{}
expected.SetEmptySecretsIfNil()
actual.SetEmptySecretsIfNil()
expected.FsConfig.Provider = dataprovider.S3FilesystemProvider
err := compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.Provider = dataprovider.LocalFilesystemProvider
expected.FsConfig.S3Config.Bucket = "bucket"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.Bucket = ""
expected.FsConfig.S3Config.Region = "region"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.Region = ""
expected.FsConfig.S3Config.AccessKey = "access key"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.AccessKey = ""
actual.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
secret, err := utils.EncryptData(secretString)
assert.NoError(t, err)
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
kmsSecret, err := kms.GetSecretFromCompatString(secret)
assert.NoError(t, err)
expected.FsConfig.S3Config.AccessSecret = kmsSecret
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, "", "", "")
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "", "data")
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "key", "")
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.AccessSecret = nil
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
expected.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.Endpoint = ""
expected.FsConfig.S3Config.StorageClass = "Standard"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.StorageClass = ""
expected.FsConfig.S3Config.KeyPrefix = "somedir/subdir"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.KeyPrefix = ""
expected.FsConfig.S3Config.UploadPartSize = 10
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.UploadPartSize = 0
expected.FsConfig.S3Config.UploadConcurrency = 3
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.S3Config.UploadConcurrency = 0
expected.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("payload")
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
expected.FsConfig.SFTPConfig.Endpoint = "endpoint"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.SFTPConfig.Endpoint = ""
expected.FsConfig.SFTPConfig.Username = "user"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.SFTPConfig.Username = ""
expected.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftppwd")
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
expected.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret("fake key")
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()
expected.FsConfig.SFTPConfig.Prefix = "/home"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.SFTPConfig.Prefix = ""
expected.FsConfig.SFTPConfig.Fingerprints = []string{"sha256:..."}
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
actual.FsConfig.SFTPConfig.Fingerprints = []string{"sha256:different"}
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
}
func TestCompareUserGCSConfig(t *testing.T) {
expected := &dataprovider.User{}
actual := &dataprovider.User{}
expected.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir"
err := compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.GCSConfig.KeyPrefix = ""
expected.FsConfig.GCSConfig.Bucket = "bucket"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.GCSConfig.Bucket = ""
expected.FsConfig.GCSConfig.StorageClass = "Standard"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.GCSConfig.StorageClass = ""
expected.FsConfig.GCSConfig.AutomaticCredentials = 1
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.GCSConfig.AutomaticCredentials = 0
}
func TestCompareUserAzureConfig(t *testing.T) {
expected := &dataprovider.User{}
actual := &dataprovider.User{}
expected.FsConfig.AzBlobConfig.Container = "a"
err := compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.Container = ""
expected.FsConfig.AzBlobConfig.AccountName = "aname"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.AccountName = ""
expected.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusAWS, "payload", "", "")
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
expected.FsConfig.AzBlobConfig.Endpoint = "endpt"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.Endpoint = ""
expected.FsConfig.AzBlobConfig.SASURL = "url"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.SASURL = ""
expected.FsConfig.AzBlobConfig.UploadPartSize = 1
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.UploadPartSize = 0
expected.FsConfig.AzBlobConfig.UploadConcurrency = 1
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.UploadConcurrency = 0
expected.FsConfig.AzBlobConfig.KeyPrefix = "prefix/"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.KeyPrefix = ""
expected.FsConfig.AzBlobConfig.UseEmulator = true
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.UseEmulator = false
expected.FsConfig.AzBlobConfig.AccessTier = "Hot"
err = compareUserFsConfig(expected, actual)
assert.Error(t, err)
expected.FsConfig.AzBlobConfig.AccessTier = ""
}
func TestGCSWebInvalidFormFile(t *testing.T) {
form := make(url.Values)
form.Set("username", "test_username")
@ -516,170 +65,313 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
assert.EqualError(t, err, http.ErrNotMultipart.Error())
}
func TestApiCallsWithBadURL(t *testing.T) {
oldBaseURL := httpBaseURL
oldAuthUsername := authUsername
oldAuthPassword := authPassword
SetBaseURLAndCredentials(invalidURL, oldAuthUsername, oldAuthPassword)
folder := vfs.BaseVirtualFolder{
MappedPath: os.TempDir(),
func TestInvalidToken(t *testing.T) {
admin := dataprovider.Admin{
Username: "admin",
}
u := dataprovider.User{}
_, _, err := UpdateUser(u, http.StatusBadRequest, "")
assert.Error(t, err)
_, err = RemoveUser(u, http.StatusNotFound)
assert.Error(t, err)
_, err = RemoveFolder(folder, http.StatusNotFound)
assert.Error(t, err)
_, _, err = GetUsers(1, 0, "", http.StatusBadRequest)
assert.Error(t, err)
_, _, err = GetFolders(1, 0, "", http.StatusBadRequest)
assert.Error(t, err)
_, err = UpdateQuotaUsage(u, "", http.StatusNotFound)
assert.Error(t, err)
_, err = UpdateFolderQuotaUsage(folder, "", http.StatusNotFound)
assert.Error(t, err)
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
assert.Error(t, err)
_, _, err = Dumpdata("backup.json", "", http.StatusBadRequest)
assert.Error(t, err)
_, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusBadRequest)
assert.Error(t, err)
_, _, err = GetBanTime("", http.StatusBadRequest)
assert.Error(t, err)
_, _, err = GetScore("", http.StatusBadRequest)
assert.Error(t, err)
SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword)
errFake := errors.New("fake error")
asJSON, err := json.Marshal(admin)
assert.NoError(t, err)
req, _ := http.NewRequest(http.MethodPut, path.Join(adminPath, admin.Username), bytes.NewBuffer(asJSON))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("username", admin.Username)
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake))
rr := httptest.NewRecorder()
updateAdmin(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
rr = httptest.NewRecorder()
deleteAdmin(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
adminPwd := adminPwd{
CurrentPassword: "old",
NewPassword: "new",
}
asJSON, err = json.Marshal(adminPwd)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPut, "", bytes.NewBuffer(asJSON))
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake))
rr = httptest.NewRecorder()
changeAdminPassword(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
adm := getAdminFromToken(req)
assert.Empty(t, adm.Username)
}
func TestApiCallToNotListeningServer(t *testing.T) {
oldBaseURL := httpBaseURL
oldAuthUsername := authUsername
oldAuthPassword := authPassword
SetBaseURLAndCredentials(inactiveURL, oldAuthUsername, oldAuthPassword)
u := dataprovider.User{}
_, _, err := AddUser(u, http.StatusBadRequest)
assert.Error(t, err)
_, _, err = UpdateUser(u, http.StatusNotFound, "")
assert.Error(t, err)
_, err = RemoveUser(u, http.StatusNotFound)
assert.Error(t, err)
_, _, err = GetUserByID(-1, http.StatusNotFound)
assert.Error(t, err)
_, _, err = GetUsers(100, 0, "", http.StatusOK)
assert.Error(t, err)
_, err = UpdateQuotaUsage(u, "", http.StatusNotFound)
assert.Error(t, err)
_, _, err = GetQuotaScans(http.StatusOK)
assert.Error(t, err)
_, err = StartQuotaScan(u, http.StatusNotFound)
assert.Error(t, err)
folder := vfs.BaseVirtualFolder{
MappedPath: os.TempDir(),
}
_, err = StartFolderQuotaScan(folder, http.StatusNotFound)
assert.Error(t, err)
_, _, err = AddFolder(folder, http.StatusOK)
assert.Error(t, err)
_, err = RemoveFolder(folder, http.StatusOK)
assert.Error(t, err)
_, _, err = GetFolders(0, 0, "", http.StatusOK)
assert.Error(t, err)
_, err = UpdateFolderQuotaUsage(folder, "", http.StatusNotFound)
assert.Error(t, err)
_, _, err = GetFoldersQuotaScans(http.StatusOK)
assert.Error(t, err)
_, _, err = GetConnections(http.StatusOK)
assert.Error(t, err)
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
assert.Error(t, err)
_, _, err = GetVersion(http.StatusOK)
assert.Error(t, err)
_, _, err = GetStatus(http.StatusOK)
assert.Error(t, err)
_, _, err = Dumpdata("backup.json", "0", http.StatusOK)
assert.Error(t, err)
_, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusOK)
assert.Error(t, err)
_, _, err = GetBanTime("", http.StatusBadRequest)
assert.Error(t, err)
_, _, err = GetScore("", http.StatusBadRequest)
assert.Error(t, err)
err = UnbanIP("", http.StatusBadRequest)
assert.Error(t, err)
func TestUpdateWebAdminInvalidClaims(t *testing.T) {
server := httpdServer{}
server.initializeRouter()
SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword)
rr := httptest.NewRecorder()
admin := dataprovider.Admin{
Username: "",
Password: "password",
}
c := jwtTokenClaims{
Username: admin.Username,
Permissions: admin.Permissions,
Signature: admin.GetSignature(),
}
token, err := c.createTokenResponse(server.tokenAuth)
assert.NoError(t, err)
form := make(url.Values)
form.Set("status", "1")
req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, "admin"), bytes.NewBuffer([]byte(form.Encode())))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("username", "admin")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
handleWebUpdateAdminPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
}
func TestBasicAuth(t *testing.T) {
oldAuthUsername := authUsername
oldAuthPassword := authPassword
authUserFile := filepath.Join(os.TempDir(), "http_users.txt")
authUserData := []byte("test1:$2y$05$bcHSED7aO1cfLto6ZdDBOOKzlwftslVhtpIkRhAtSa4GuLmk5mola\n")
err := ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
assert.NoError(t, err)
httpAuth, _ = common.NewBasicAuthProvider(authUserFile)
_, _, err = GetVersion(http.StatusUnauthorized)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test1", "password1")
_, _, err = GetVersion(http.StatusOK)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test1", "wrong_password")
resp, _ := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(metricsPath), nil, "")
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
authUserData = append(authUserData, []byte("test2:$1$OtSSTL8b$bmaCqEksI1e7rnZSjsIDR1\n")...)
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
_, _, err = GetVersion(http.StatusOK)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test2", "wrong_password")
_, _, err = GetVersion(http.StatusOK)
assert.Error(t, err)
authUserData = append(authUserData, []byte("test2:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...)
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
_, _, err = GetVersion(http.StatusOK)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test2", "wrong_password")
_, _, err = GetVersion(http.StatusOK)
assert.Error(t, err)
authUserData = append(authUserData, []byte("test3:$apr1$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...)
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test3", "wrong_password")
_, _, err = GetVersion(http.StatusUnauthorized)
assert.NoError(t, err)
authUserData = append(authUserData, []byte("test4:$invalid$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...)
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test3", "password2")
_, _, err = GetVersion(http.StatusUnauthorized)
assert.NoError(t, err)
if runtime.GOOS != osWindows {
authUserData = append(authUserData, []byte("test5:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...)
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
assert.NoError(t, err)
err = os.Chmod(authUserFile, 0001)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test5", "password2")
_, _, err = GetVersion(http.StatusUnauthorized)
assert.NoError(t, err)
err = os.Chmod(authUserFile, os.ModePerm)
assert.NoError(t, err)
func TestCreateTokenError(t *testing.T) {
server := httpdServer{
tokenAuth: jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil),
}
authUserData = append(authUserData, []byte("\"foo\"bar\"\r\n")...)
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
rr := httptest.NewRecorder()
admin := dataprovider.Admin{
Username: "admin",
Password: "password",
}
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
server.checkAddrAndSendToken(rr, req, admin)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
rr = httptest.NewRecorder()
form := make(url.Values)
form.Set("username", admin.Username)
form.Set("password", admin.Password)
req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))
req.RemoteAddr = "127.0.0.1:1234"
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
server.handleWebLoginPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
// req with no content type
req, _ = http.NewRequest(http.MethodPost, webLoginPath, nil)
rr = httptest.NewRecorder()
server.handleWebLoginPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
// req with no POST body
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%AO%GG", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder()
server.handleWebLoginPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A1%G2", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder()
handleWebAdminChangePwdPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A2%G3", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
_, err := getAdminFromPostFields(req)
assert.Error(t, err)
}
func TestJWTTokenValidation(t *testing.T) {
tokenAuth := jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil)
claims := make(map[string]interface{})
claims["username"] = "admin"
claims[jwt.ExpirationKey] = time.Now().UTC().Add(-1 * time.Hour)
token, _, err := tokenAuth.Encode(claims)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
_, _, err = GetVersion(http.StatusUnauthorized)
r := GetHTTPRouter()
fn := jwtAuthenticator(r)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, userPath, nil)
ctx := jwtauth.NewContext(req.Context(), token, nil)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusUnauthorized, rr.Code)
fn = jwtAuthenticatorWeb(r)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
ctx = jwtauth.NewContext(req.Context(), token, nil)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusFound, rr.Code)
errTest := errors.New("test error")
permFn := checkPerm(dataprovider.PermAdminAny)
fn = permFn(r)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, userPath, nil)
ctx = jwtauth.NewContext(req.Context(), token, errTest)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusBadRequest, rr.Code)
permFn = checkPerm(dataprovider.PermAdminAny)
fn = permFn(r)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
req.RequestURI = webUserPath
ctx = jwtauth.NewContext(req.Context(), token, errTest)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestAdminAllowListConnAddr(t *testing.T) {
server := httpdServer{}
admin := dataprovider.Admin{
Filters: dataprovider.AdminFilters{
AllowList: []string{"192.168.1.0/24"},
},
}
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
ctx := context.WithValue(req.Context(), connAddrKey, "127.0.0.1:4567")
req.RemoteAddr = "192.168.1.16:1234"
server.checkAddrAndSendToken(rr, req.WithContext(ctx), admin)
assert.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String())
}
func TestUpdateContextFromCookie(t *testing.T) {
server := httpdServer{
tokenAuth: jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil),
}
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
claims := make(map[string]interface{})
claims["a"] = "b"
token, _, err := server.tokenAuth.Encode(claims)
assert.NoError(t, err)
err = os.Remove(authUserFile)
ctx := jwtauth.NewContext(req.Context(), token, nil)
server.updateContextFromCookie(req.WithContext(ctx))
}
func TestCookieExpiration(t *testing.T) {
server := httpdServer{
tokenAuth: jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil),
}
err := errors.New("test error")
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
ctx := jwtauth.NewContext(req.Context(), nil, err)
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie := rr.Header().Get("Set-Cookie")
assert.Empty(t, cookie)
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
claims := make(map[string]interface{})
claims["a"] = "b"
token, _, err := server.tokenAuth.Encode(claims)
assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, oldAuthUsername, oldAuthPassword)
httpAuth, _ = common.NewBasicAuthProvider("")
ctx = jwtauth.NewContext(req.Context(), token, nil)
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie = rr.Header().Get("Set-Cookie")
assert.Empty(t, cookie)
admin := dataprovider.Admin{
Username: "newtestadmin",
Password: "password",
Permissions: []string{dataprovider.PermAdminAny},
}
claims = make(map[string]interface{})
claims[claimUsernameKey] = admin.Username
claims[claimPermissionsKey] = admin.Permissions
claims[jwt.SubjectKey] = admin.GetSignature()
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
token, _, err = server.tokenAuth.Encode(claims)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
ctx = jwtauth.NewContext(req.Context(), token, nil)
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie = rr.Header().Get("Set-Cookie")
assert.Empty(t, cookie)
admin.Status = 0
err = dataprovider.AddAdmin(&admin)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
ctx = jwtauth.NewContext(req.Context(), token, nil)
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie = rr.Header().Get("Set-Cookie")
assert.Empty(t, cookie)
admin.Status = 1
admin.Filters.AllowList = []string{"172.16.1.0/24"}
err = dataprovider.UpdateAdmin(&admin)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
ctx = jwtauth.NewContext(req.Context(), token, nil)
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie = rr.Header().Get("Set-Cookie")
assert.Empty(t, cookie)
admin, err = dataprovider.AdminExists(admin.Username)
assert.NoError(t, err)
claims = make(map[string]interface{})
claims[claimUsernameKey] = admin.Username
claims[claimPermissionsKey] = admin.Permissions
claims[jwt.SubjectKey] = admin.GetSignature()
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
token, _, err = server.tokenAuth.Encode(claims)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
req.RemoteAddr = "192.168.8.1:1234"
ctx = jwtauth.NewContext(req.Context(), token, nil)
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie = rr.Header().Get("Set-Cookie")
assert.Empty(t, cookie)
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
req.RemoteAddr = "172.16.1.2:1234"
ctx = jwtauth.NewContext(req.Context(), token, nil)
ctx = context.WithValue(ctx, connAddrKey, "10.9.9.9")
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie = rr.Header().Get("Set-Cookie")
assert.Empty(t, cookie)
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
req.RemoteAddr = "172.16.1.12:4567"
ctx = jwtauth.NewContext(req.Context(), token, nil)
server.checkCookieExpiration(rr, req.WithContext(ctx))
cookie = rr.Header().Get("Set-Cookie")
assert.True(t, strings.HasPrefix(cookie, "jwt="))
err = dataprovider.DeleteAdmin(admin.Username)
assert.NoError(t, err)
}
func TestGetURLParam(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, adminPwdPath, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("val", "testuser%C3%A0")
rctx.URLParams.Add("inval", "testuser%C3%AO%GG")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
escaped := getURLParam(req, "val")
assert.Equal(t, "testuserà", escaped)
escaped = getURLParam(req, "inval")
assert.Equal(t, "testuser%C3%AO%GG", escaped)
}
func TestChangePwdValidationErrors(t *testing.T) {
err := doChangeAdminPassword(nil, "", "", "")
require.Error(t, err)
err = doChangeAdminPassword(nil, "a", "b", "c")
require.Error(t, err)
err = doChangeAdminPassword(nil, "a", "a", "a")
require.Error(t, err)
req, _ := http.NewRequest(http.MethodPut, adminPwdPath, nil)
err = doChangeAdminPassword(req, "currentpwd", "newpwd", "newpwd")
assert.Error(t, err)
}
func TestRenderUnexistingFolder(t *testing.T) {
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, folderPath, nil)
renderFolder(rr, req, "path not mapped")
assert.Equal(t, http.StatusNotFound, rr.Code)
}
func TestCloseConnectionHandler(t *testing.T) {

95
httpd/middleware.go Normal file
View file

@ -0,0 +1,95 @@
package httpd
import (
"context"
"net/http"
"github.com/go-chi/jwtauth"
"github.com/lestrrat-go/jwx/jwt"
"github.com/drakkan/sftpgo/logger"
)
type ctxKeyConnAddr int
const connAddrKey ctxKeyConnAddr = 0
func saveConnectionAddress(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), connAddrKey, r.RemoteAddr)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func jwtAuthenticator(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, _, err := jwtauth.FromContext(r.Context())
if err != nil {
logger.Debug(logSender, "", "error getting jwt token: %v", err)
sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
err = jwt.Validate(token)
if token == nil || err != nil {
logger.Debug(logSender, "", "error validating jwt token: %v", err)
sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
// Token is authenticated, pass it through
next.ServeHTTP(w, r)
})
}
func jwtAuthenticatorWeb(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, _, err := jwtauth.FromContext(r.Context())
if err != nil {
logger.Debug(logSender, "", "error getting web jwt token: %v", err)
http.Redirect(w, r, webLoginPath, http.StatusFound)
return
}
err = jwt.Validate(token)
if token == nil || err != nil {
logger.Debug(logSender, "", "error validating web jwt token: %v", err)
http.Redirect(w, r, webLoginPath, http.StatusFound)
return
}
// Token is authenticated, pass it through
next.ServeHTTP(w, r)
})
}
func checkPerm(perm string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
if isWebAdminRequest(r) {
renderBadRequestPage(w, r, err)
} else {
sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
return
}
tokenClaims := jwtTokenClaims{}
tokenClaims.Decode(claims)
if !tokenClaims.hasPerm(perm) {
if isWebAdminRequest(r) {
renderForbiddenPage(w, r, "You don't have permission for this action")
} else {
sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
}
return
}
next.ServeHTTP(w, r)
})
}
}

View file

@ -1,138 +0,0 @@
package httpd
import (
"net/http"
"strings"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/metrics"
"github.com/drakkan/sftpgo/version"
)
// GetHTTPRouter returns the configured HTTP handler
func GetHTTPRouter() http.Handler {
return router
}
func initializeRouter(staticFilesPath string, enableWebAdmin bool) {
router = chi.NewRouter()
router.Use(middleware.GetHead)
router.Group(func(r chi.Router) {
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
render.PlainText(w, r, "ok")
})
})
router.Group(func(router chi.Router) {
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
router.Use(logger.NewStructuredLogger(logger.GetLogger()))
router.Use(middleware.Recoverer)
router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
}))
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently)
})
router.Group(func(router chi.Router) {
router.Use(checkAuth)
router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently)
})
metrics.AddMetricsEndpoint(metricsPath, router)
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, version.Get())
})
router.Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, getServicesStatus())
})
router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, common.Connections.GetStats())
})
router.Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
router.Get(quotaScanPath, getQuotaScans)
router.Post(quotaScanPath, startQuotaScan)
router.Get(quotaScanVFolderPath, getVFolderQuotaScans)
router.Post(quotaScanVFolderPath, startVFolderQuotaScan)
router.Get(userPath, getUsers)
router.Post(userPath, addUser)
router.Get(userPath+"/{userID}", getUserByID)
router.Put(userPath+"/{userID}", updateUser)
router.Delete(userPath+"/{userID}", deleteUser)
router.Get(folderPath, getFolders)
router.Post(folderPath, addFolder)
router.Delete(folderPath, deleteFolderByPath)
router.Get(dumpDataPath, dumpData)
router.Get(loadDataPath, loadData)
router.Put(updateUsedQuotaPath, updateUserQuotaUsage)
router.Put(updateFolderUsedQuotaPath, updateVFolderQuotaUsage)
router.Get(defenderBanTime, getBanTime)
router.Get(defenderScore, getScore)
router.Post(defenderUnban, unban)
if enableWebAdmin {
router.Get(webUsersPath, handleGetWebUsers)
router.Get(webUserPath, handleWebAddUserGet)
router.Get(webUserPath+"/{userID}", handleWebUpdateUserGet)
router.Post(webUserPath, handleWebAddUserPost)
router.Post(webUserPath+"/{userID}", handleWebUpdateUserPost)
router.Get(webConnectionsPath, handleWebGetConnections)
router.Get(webFoldersPath, handleWebGetFolders)
router.Get(webFolderPath, handleWebAddFolderGet)
router.Post(webFolderPath, handleWebAddFolderPost)
router.Get(webStatusPath, handleWebGetStatus)
}
})
if enableWebAdmin {
router.Group(func(router chi.Router) {
compressor := middleware.NewCompressor(5)
router.Use(compressor.Handler)
fileServer(router, webStaticFilesPath, http.Dir(staticFilesPath))
})
}
})
}
func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
connectionID := chi.URLParam(r, "connectionID")
if connectionID == "" {
sendAPIResponse(w, r, nil, "connectionID is mandatory", http.StatusBadRequest)
return
}
if common.Connections.Close(connectionID) {
sendAPIResponse(w, r, nil, "Connection closed", http.StatusOK)
} else {
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
}
}
func fileServer(r chi.Router, path string, root http.FileSystem) {
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
path += "/"
}
path += "*"
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}

View file

@ -2,12 +2,12 @@ openapi: 3.0.3
info:
title: SFTPGo
description: SFTPGo REST API
version: 2.3.0
version: 2.4.0
servers:
- url: /api/v1
- url: /api/v2
security:
- BasicAuth: []
- BearerAuth: []
paths:
/healthz:
get:
@ -26,6 +26,29 @@ paths:
schema:
type: string
example: ok
/token:
get:
security:
- BasicAuth: []
tags:
- token
summary: Get an access token
operationId: get_token
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref : '#/components/schemas/Token'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/version:
get:
tags:
@ -47,7 +70,34 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/connection:
/changepwd/admin:
put:
tags:
- admins
summary: Change the password for the logged in admin
operationId: change_admin_password
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PwdChange'
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref : '#/components/schemas/ApiResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/connections:
get:
tags:
- connections
@ -70,7 +120,7 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/connection/{connectionID}:
/connections/{connectionID}:
delete:
tags:
- connections
@ -102,7 +152,7 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/defender/ban_time:
/defender/bantime:
get:
tags:
- defender
@ -197,7 +247,7 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quota_scan:
/quota-scans:
get:
tags:
- quota
@ -255,7 +305,7 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quota_update:
/quota-update:
put:
tags:
- quota
@ -305,7 +355,7 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/folder_quota_update:
/folder-quota-update:
put:
tags:
- quota
@ -355,7 +405,7 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/folder_quota_scan:
/folder-quota-scans:
get:
tags:
- quota
@ -413,7 +463,7 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/folder:
/folders:
get:
tags:
- folders
@ -484,7 +534,7 @@ paths:
schema:
$ref : '#/components/schemas/BaseVirtualFolder'
responses:
200:
201:
description: successful operation
content:
application/json:
@ -533,7 +583,193 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/user:
/admins:
get:
tags:
- admins
summary: Returns an array with one or more admins
description: For security reasons hashed passwords are omitted in the response
operationId: get_admins
parameters:
- in: query
name: offset
schema:
type: integer
minimum: 0
default: 0
required: false
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 500
default: 100
required: false
description: The maximum number of items to return. Max value is 500, default is 100
- in: query
name: order
required: false
description: Ordering admins by username. Default ASC
schema:
type: string
enum:
- ASC
- DESC
example: ASC
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref : '#/components/schemas/Admin'
400:
$ref: '#/components/responses/BadRequest'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
post:
tags:
- admins
summary: Adds a new admin
operationId: add_admin
requestBody:
required: true
content:
application/json:
schema:
$ref : '#/components/schemas/Admin'
responses:
201:
description: successful operation
content:
application/json:
schema:
$ref : '#/components/schemas/Admin'
400:
$ref: '#/components/responses/BadRequest'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/admins/{username}:
get:
tags:
- admins
summary: Find admin by username
description: For security reasons the hashed password is omitted in the response
operationId: get_admin_by_username
parameters:
- name: username
in: path
description: username of the admin to retrieve
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref : '#/components/schemas/Admin'
400:
$ref: '#/components/responses/BadRequest'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
put:
tags:
- admins
summary: Update an existing admin
operationId: update_admin
parameters:
- name: username
in: path
description: username of the admin to update
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref : '#/components/schemas/Admin'
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref : '#/components/schemas/ApiResponse'
example:
message: "User updated"
400:
$ref: '#/components/responses/BadRequest'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
delete:
tags:
- admins
summary: Delete an existing admin
operationId: delete_admin
parameters:
- name: username
in: path
description: username of the admin to delete
required: true
schema:
type: string
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref : '#/components/schemas/ApiResponse'
example:
message: "User deleted"
400:
$ref: '#/components/responses/BadRequest'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/users:
get:
tags:
- users
@ -567,12 +803,6 @@ paths:
- ASC
- DESC
example: ASC
- in: query
name: username
required: false
description: Filter by username, extact match case sensitive
schema:
type: string
responses:
200:
description: successful operation
@ -604,7 +834,7 @@ paths:
schema:
$ref : '#/components/schemas/User'
responses:
200:
201:
description: successful operation
content:
application/json:
@ -620,21 +850,20 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/user/{userID}:
/users/{username}:
get:
tags:
- users
summary: Find user by ID
summary: Find user by username
description: For security reasons the hashed password is omitted in the response
operationId: get_user_by_id
operationId: get_user_by_username
parameters:
- name: userID
- name: username
in: path
description: ID of the user to retrieve
description: username of the user to retrieve
required: true
schema:
type: integer
format: int32
type: string
responses:
200:
description: successful operation
@ -660,13 +889,12 @@ paths:
summary: Update an existing user
operationId: update_user
parameters:
- name: userID
- name: username
in: path
description: ID of the user to update
description: username of the user to update
required: true
schema:
type: integer
format: int32
type: string
- in: query
name: disconnect
schema:
@ -711,13 +939,12 @@ paths:
summary: Delete an existing user
operationId: delete_user
parameters:
- name: userID
- name: username
in: path
description: ID of the user to delete
description: username of the user to delete
required: true
schema:
type: integer
format: int32
type: string
responses:
200:
description: successful operation
@ -949,6 +1176,22 @@ components:
minItems: 1
minProperties: 1
description: hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory ("/") are required
AdminPermissions:
type: string
enum:
- '*'
- 'add_users'
- 'edit_users'
- 'del_users'
- 'view_users'
- 'view_conns'
- 'close_conns'
- 'view_status'
- 'manage_admins'
- 'quota_scans'
- 'manage_system'
- 'manage_defender'
- 'view_defender'
LoginMethods:
type: string
enum:
@ -975,14 +1218,12 @@ components:
type: array
items:
type: string
nullable: true
description: list of, case insensitive, allowed shell like file patterns.
example: [ "*.jpg", "a*b?.png" ]
denied_patterns:
type: array
items:
type: string
nullable: true
description: list of, case insensitive, denied shell like file patterns. Denied patterns are evaluated before the allowed ones
example: [ "*.zip" ]
ExtensionsFilter:
@ -995,14 +1236,12 @@ components:
type: array
items:
type: string
nullable: true
description: list of, case insensitive, allowed files extension. Shell like expansion is not supported so you have to specify `.jpg` and not `*.jpg`
example: [ ".jpg", ".png" ]
denied_extensions:
type: array
items:
type: string
nullable: true
description: list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones
example: [ ".zip" ]
UserFilters:
@ -1012,44 +1251,37 @@ components:
type: array
items:
type: string
nullable: true
description: only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32"
example: [ "192.0.2.0/24", "2001:db8::/32" ]
denied_ip:
type: array
items:
type: string
nullable: true
description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones
example: [ "172.16.0.0/16" ]
denied_login_methods:
type: array
items:
$ref: '#/components/schemas/LoginMethods'
nullable: true
description: if null or empty any available login method is allowed
denied_protocols:
type: array
items:
$ref: '#/components/schemas/SupportedProtocols'
nullable: true
description: if null or empty any available protocol is allowed
file_patterns:
type: array
items:
$ref: '#/components/schemas/PatternsFilter'
nullable: true
description: filters based on shell like file patterns. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be in the list of files. Please note that these restrictions can be easily bypassed
file_extensions:
type: array
items:
$ref: '#/components/schemas/ExtensionsFilter'
nullable: true
description: filters based on shell like patterns. Deprecated, use file_patterns. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be in the list of files. Please note that these restrictions can be easily bypassed
max_upload_file_size:
type: integer
format: int64
nullable: true
description: maximum allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited. This restriction does not apply for SSH system commands such as `git` and `rsync`
description: Additional restrictions
Secret:
@ -1106,7 +1338,6 @@ components:
required:
- bucket
- region
nullable: true
description: S3 Compatible Object Storage configuration details
GCSConfig:
type: object
@ -1118,7 +1349,6 @@ components:
$ref: '#/components/schemas/Secret'
automatic_credentials:
type: integer
nullable: true
enum:
- 0
- 1
@ -1134,7 +1364,6 @@ components:
example: folder/subfolder/
required:
- bucket
nullable: true
description: Google Cloud Storage configuration details. The "credentials" field must be populated only when adding/updating a user. It will be always omitted, since there are sensitive data, when you search/get users
AzureBlobFsConfig:
type: object
@ -1171,7 +1400,6 @@ components:
example: folder/subfolder/
use_emulator:
type: boolean
nullable: true
description: Azure Blob Storage configuration details
CryptFsConfig:
type: object
@ -1253,7 +1481,6 @@ components:
description: Last quota update as unix timestamp in milliseconds
users:
type: array
nullable: true
items:
type: string
description: list of usernames associated with this virtual folder
@ -1303,13 +1530,12 @@ components:
description: expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration
password:
type: string
nullable: true
format: password
description: password or public key/SSH user certificate are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
public_keys:
type: array
items:
type: string
nullable: true
description: a password or at least one public key/SSH user certificate are mandatory.
home_dir:
type: string
@ -1318,7 +1544,6 @@ components:
type: array
items:
$ref: '#/components/schemas/VirtualFolder'
nullable: true
description: mapping between virtual SFTPGo paths and filesystem paths outside the user home directory. Supported for local filesystem only. If one or more of the specified folders are not inside the dataprovider they will be automatically created. You have to create the folder on the filesystem yourself
uid:
type: integer
@ -1379,6 +1604,50 @@ components:
additional_info:
type: string
description: Free form text field for external systems
AdminFilters:
type: object
properties:
allow_list:
type: array
items:
type: string
description: only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32"
example: [ "192.0.2.0/24", "2001:db8::/32" ]
Admin:
type: object
properties:
id:
type: integer
format: int32
minimum: 1
status:
type: integer
enum:
- 0
- 1
description: >
status:
* `0` user is disabled, login is not allowed
* `1` user is enabled
username:
type: string
description: username is unique
password:
type: string
format: password
description: Admin password. For security reasons this field is omitted when you search/get admins
email:
type: string
format: email
permissions:
type: array
items:
$ref: '#/components/schemas/AdminPermissions'
filters:
$ref: '#/components/schemas/AdminFilters'
additional_info:
type: string
description: Free form text field
Transfer:
type: object
properties:
@ -1409,7 +1678,6 @@ components:
description: unique connection identifier
client_version:
type: string
nullable: true
description: client version
remote_address:
type: string
@ -1420,7 +1688,6 @@ components:
description: connection time as unix timestamp in milliseconds
command:
type: string
nullable: true
description: SSH/FTP command or WebDAV method
last_activity:
type: integer
@ -1436,7 +1703,6 @@ components:
- DAV
active_transfers:
type: array
nullable: true
items:
$ref : '#/components/schemas/Transfer'
QuotaScan:
@ -1602,6 +1868,13 @@ components:
score:
type: integer
description: if 0 the host is not listed
PwdChange:
type: object
properties:
current_password:
type: string
new_password:
type: string
ApiResponse:
type: object
properties:
@ -1610,7 +1883,6 @@ components:
description: message, can be empty
error:
type: string
nullable: true
description: error description if any
VersionInfo:
type: object
@ -1626,7 +1898,19 @@ components:
items:
type: string
description: Features for the current build. Available features are "portable", "bolt", "mysql", "sqlite", "pgsql", "s3", "gcs", "metrics". If a feature is available it has a "+" prefix, otherwise a "-" prefix
Token:
type: object
properties:
access_token:
type: string
expires_at:
type: string
format: date-time
securitySchemes:
BasicAuth:
type: http
scheme: basic
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT

346
httpd/server.go Normal file
View file

@ -0,0 +1,346 @@
package httpd
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/jwtauth"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/version"
)
type httpdServer struct {
binding Binding
staticFilesPath string
enableWebAdmin bool
router *chi.Mux
tokenAuth *jwtauth.JWTAuth
}
func newHttpdServer(bindAddress string, bindPort int, staticFilesPath string, enableWebAdmin bool) *httpdServer {
return &httpdServer{
binding: Binding{
Address: bindAddress,
Port: bindPort,
},
staticFilesPath: staticFilesPath,
enableWebAdmin: enableWebAdmin,
}
}
func (s *httpdServer) listenAndServe() error {
s.initializeRouter()
httpServer := &http.Server{
Handler: s.router,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 16, // 64KB
ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
}
if certMgr != nil {
config := &tls.Config{
GetCertificate: certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12,
}
httpServer.TLSConfig = config
return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true, logSender)
}
return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false, logSender)
}
func (s *httpdServer) refreshCookie(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.checkCookieExpiration(w, r)
next.ServeHTTP(w, r)
})
}
func (s *httpdServer) handleWebLoginPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if err := r.ParseForm(); err != nil {
renderLoginPage(w, err.Error())
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
if username == "" || password == "" {
renderLoginPage(w, "Invalid credentials")
return
}
admin, err := dataprovider.CheckAdminAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
renderLoginPage(w, err.Error())
return
}
if connAddr, ok := r.Context().Value(connAddrKey).(string); ok {
if connAddr != r.RemoteAddr {
if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(connAddr)) {
renderLoginPage(w, fmt.Sprintf("Login from IP %v is not allowed", connAddr))
return
}
}
}
c := jwtTokenClaims{
Username: admin.Username,
Permissions: admin.Permissions,
Signature: admin.GetSignature(),
}
err = c.createAndSetCookie(w, s.tokenAuth)
if err != nil {
renderLoginPage(w, err.Error())
return
}
http.Redirect(w, r, webUsersPath, http.StatusFound)
}
func (s *httpdServer) getToken(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
admin, err := dataprovider.CheckAdminAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
s.checkAddrAndSendToken(w, r, admin)
}
func (s *httpdServer) checkAddrAndSendToken(w http.ResponseWriter, r *http.Request, admin dataprovider.Admin) {
if connAddr, ok := r.Context().Value(connAddrKey).(string); ok {
if connAddr != r.RemoteAddr {
if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(connAddr)) {
sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
}
}
c := jwtTokenClaims{
Username: admin.Username,
Permissions: admin.Permissions,
Signature: admin.GetSignature(),
}
resp, err := c.createTokenResponse(s.tokenAuth)
if err != nil {
sendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
render.JSON(w, r, resp)
}
func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Request) {
token, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
return
}
tokenClaims := jwtTokenClaims{}
tokenClaims.Decode(claims)
if tokenClaims.Username == "" || tokenClaims.Signature == "" {
return
}
if time.Until(token.Expiration()) > tokenRefreshMin {
return
}
admin, err := dataprovider.AdminExists(tokenClaims.Username)
if err != nil {
return
}
if admin.Status != 1 {
logger.Debug(logSender, "", "admin %#v is disabled, unable to refresh cookie", admin.Username)
return
}
if admin.GetSignature() != tokenClaims.Signature {
logger.Debug(logSender, "", "signature mismatch for admin %#v, unable to refresh cookie", admin.Username)
return
}
if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(r.RemoteAddr)) {
logger.Debug(logSender, "", "admin %#v cannot login from %v, unable to refresh cookie", admin.Username, r.RemoteAddr)
return
}
if connAddr, ok := r.Context().Value(connAddrKey).(string); ok {
if connAddr != r.RemoteAddr {
if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(connAddr)) {
logger.Debug(logSender, "", "admin %#v cannot login from %v, unable to refresh cookie",
admin.Username, connAddr)
return
}
}
}
logger.Debug(logSender, "", "cookie refreshed for admin %#v", admin.Username)
tokenClaims.createAndSetCookie(w, s.tokenAuth) //nolint:errcheck
}
func (s *httpdServer) updateContextFromCookie(r *http.Request) *http.Request {
token, _, err := jwtauth.FromContext(r.Context())
if token == nil || err != nil {
_, err = r.Cookie("jwt")
if err != nil {
return r
}
token, err = jwtauth.VerifyRequest(s.tokenAuth, r, jwtauth.TokenFromCookie)
ctx := jwtauth.NewContext(r.Context(), token, err)
return r.WithContext(ctx)
}
return r
}
func (s *httpdServer) initializeRouter() {
s.tokenAuth = jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil)
s.router = chi.NewRouter()
s.router.Use(saveConnectionAddress)
s.router.Use(middleware.GetHead)
s.router.Group(func(r chi.Router) {
r.Get(healthzPath, func(w http.ResponseWriter, r *http.Request) {
render.PlainText(w, r, "ok")
})
})
s.router.Group(func(router chi.Router) {
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
router.Use(logger.NewStructuredLogger(logger.GetLogger()))
router.Use(middleware.Recoverer)
router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.enableWebAdmin && isWebAdminRequest(r) {
r = s.updateContextFromCookie(r)
renderNotFoundPage(w, r, nil)
return
}
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
}))
router.Get(tokenPath, s.getToken)
router.Group(func(router chi.Router) {
router.Use(jwtauth.Verifier(s.tokenAuth))
router.Use(jwtAuthenticator)
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, version.Get())
})
router.Put(adminPwdPath, changeAdminPassword)
router.With(checkPerm(dataprovider.PermAdminViewServerStatus)).
Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, getServicesStatus())
})
router.With(checkPerm(dataprovider.PermAdminViewConnections)).
Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, common.Connections.GetStats())
})
router.With(checkPerm(dataprovider.PermAdminCloseConnections)).
Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanPath, getQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanPath, startQuotaScan)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanVFolderPath, getVFolderQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanVFolderPath, startVFolderQuotaScan)
router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath, getFolders)
router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder)
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath, deleteFolderByPath)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateUsedQuotaPath, updateUserQuotaUsage)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateFolderUsedQuotaPath, updateVFolderQuotaUsage)
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderBanTime, getBanTime)
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderScore, getScore)
router.With(checkPerm(dataprovider.PermAdminManageDefender)).Post(defenderUnban, unban)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
})
if s.enableWebAdmin {
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, webLoginPath, http.StatusMovedPermanently)
})
router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, webLoginPath, http.StatusMovedPermanently)
})
router.Get(webLoginPath, handleWebLogin)
router.Post(webLoginPath, s.handleWebLoginPost)
router.Group(func(router chi.Router) {
router.Use(jwtauth.Verifier(s.tokenAuth))
router.Use(jwtAuthenticatorWeb)
router.Get(webLogoutPath, handleWebLogout)
router.With(s.refreshCookie).Get(webChangeAdminPwdPath, handleWebAdminChangePwd)
router.Post(webChangeAdminPwdPath, handleWebAdminChangePwdPost)
router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
Get(webUsersPath, handleGetWebUsers)
router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
Get(webUserPath, handleWebAddUserGet)
router.With(checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
Get(webUserPath+"/{username}", handleWebUpdateUserGet)
router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, handleWebAddUserPost)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}", handleWebUpdateUserPost)
router.With(checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
Get(webConnectionsPath, handleWebGetConnections)
router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
Get(webFoldersPath, handleWebGetFolders)
router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
Get(webFolderPath, handleWebAddFolderGet)
router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(webFolderPath, handleWebAddFolderPost)
router.With(checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie).
Get(webStatusPath, handleWebGetStatus)
router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
Get(webAdminsPath, handleGetWebAdmins)
router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
Get(webAdminPath, handleWebAddAdminGet)
router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
Get(webAdminPath+"/{username}", handleWebUpdateAdminGet)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, handleWebAddAdminPost)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}", handleWebUpdateAdminPost)
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(webAdminPath+"/{username}", deleteAdmin)
router.With(checkPerm(dataprovider.PermAdminCloseConnections)).
Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(webFolderPath, deleteFolderByPath)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webScanVFolderPath, startVFolderQuotaScan)
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(webUserPath+"/{username}", deleteUser)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webQuotaScanPath, startQuotaScan)
})
router.Group(func(router chi.Router) {
compressor := middleware.NewCompressor(5)
router.Use(compressor.Handler)
fileServer(router, webStaticFilesPath, http.Dir(s.staticFilesPath))
})
}
})
}

View file

@ -6,14 +6,13 @@ import (
"html/template"
"io/ioutil"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-chi/chi"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/kms"
@ -26,16 +25,23 @@ const (
templateBase = "base.html"
templateUsers = "users.html"
templateUser = "user.html"
templateAdmins = "admins.html"
templateAdmin = "admin.html"
templateConnections = "connections.html"
templateFolders = "folders.html"
templateFolder = "folder.html"
templateMessage = "message.html"
templateStatus = "status.html"
templateLogin = "login.html"
templateChangePwd = "changepwd.html"
pageUsersTitle = "Users"
pageAdminsTitle = "Admins"
pageConnectionsTitle = "Connections"
pageStatusTitle = "Status"
pageFoldersTitle = "Folders"
pageChangePwdTitle = "Change password"
page400Title = "Bad request"
page403Title = "Forbidden"
page404Title = "Not found"
page404Body = "The page you are looking for does not exist."
page500Title = "Internal Server Error"
@ -50,24 +56,27 @@ var (
)
type basePage struct {
Title string
CurrentURL string
UsersURL string
UserURL string
APIUserURL string
APIConnectionsURL string
APIQuotaScanURL string
ConnectionsURL string
FoldersURL string
FolderURL string
APIFoldersURL string
APIFolderQuotaScanURL string
StatusURL string
UsersTitle string
ConnectionsTitle string
FoldersTitle string
StatusTitle string
Version string
Title string
CurrentURL string
UsersURL string
UserURL string
AdminsURL string
AdminURL string
QuotaScanURL string
ConnectionsURL string
FoldersURL string
FolderURL string
LogoutURL string
ChangeAdminPwdURL string
FolderQuotaScanURL string
StatusURL string
UsersTitle string
AdminsTitle string
ConnectionsTitle string
FoldersTitle string
StatusTitle string
Version string
LoggedAdmin *dataprovider.Admin
}
type usersPage struct {
@ -75,6 +84,11 @@ type usersPage struct {
Users []dataprovider.User
}
type adminsPage struct {
basePage
Admins []dataprovider.Admin
}
type foldersPage struct {
basePage
Folders []vfs.BaseVirtualFolder
@ -103,6 +117,18 @@ type userPage struct {
IsAdd bool
}
type adminPage struct {
basePage
Admin *dataprovider.Admin
Error string
IsAdd bool
}
type changePwdPage struct {
basePage
Error string
}
type folderPage struct {
basePage
Folder vfs.BaseVirtualFolder
@ -115,6 +141,12 @@ type messagePage struct {
Success string
}
type loginPage struct {
CurrentURL string
Version string
Error string
}
func loadTemplates(templatesPath string) {
usersPaths := []string{
filepath.Join(templatesPath, templateBase),
@ -124,6 +156,18 @@ func loadTemplates(templatesPath string) {
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateUser),
}
adminsPaths := []string{
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateAdmins),
}
adminPaths := []string{
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateAdmin),
}
changePwdPaths := []string{
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateChangePwd),
}
connectionsPaths := []string{
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateConnections),
@ -144,43 +188,57 @@ func loadTemplates(templatesPath string) {
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateStatus),
}
loginPath := []string{
filepath.Join(templatesPath, templateLogin),
}
usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
adminsTmpl := utils.LoadTemplate(template.ParseFiles(adminsPaths...))
adminTmpl := utils.LoadTemplate(template.ParseFiles(adminPaths...))
connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...))
messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...))
foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...))
folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...))
statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...))
loginTmpl := utils.LoadTemplate(template.ParseFiles(loginPath...))
changePwdTmpl := utils.LoadTemplate(template.ParseFiles(changePwdPaths...))
templates[templateUsers] = usersTmpl
templates[templateUser] = userTmpl
templates[templateAdmins] = adminsTmpl
templates[templateAdmin] = adminTmpl
templates[templateConnections] = connectionsTmpl
templates[templateMessage] = messageTmpl
templates[templateFolders] = foldersTmpl
templates[templateFolder] = folderTmpl
templates[templateStatus] = statusTmpl
templates[templateLogin] = loginTmpl
templates[templateChangePwd] = changePwdTmpl
}
func getBasePageData(title, currentURL string) basePage {
func getBasePageData(title, currentURL string, r *http.Request) basePage {
return basePage{
Title: title,
CurrentURL: currentURL,
UsersURL: webUsersPath,
UserURL: webUserPath,
FoldersURL: webFoldersPath,
FolderURL: webFolderPath,
APIUserURL: userPath,
APIConnectionsURL: activeConnectionsPath,
APIQuotaScanURL: quotaScanPath,
APIFoldersURL: folderPath,
APIFolderQuotaScanURL: quotaScanVFolderPath,
ConnectionsURL: webConnectionsPath,
StatusURL: webStatusPath,
UsersTitle: pageUsersTitle,
ConnectionsTitle: pageConnectionsTitle,
FoldersTitle: pageFoldersTitle,
StatusTitle: pageStatusTitle,
Version: version.GetAsString(),
Title: title,
CurrentURL: currentURL,
UsersURL: webUsersPath,
UserURL: webUserPath,
AdminsURL: webAdminsPath,
AdminURL: webAdminPath,
FoldersURL: webFoldersPath,
FolderURL: webFolderPath,
LogoutURL: webLogoutPath,
ChangeAdminPwdURL: webChangeAdminPwdPath,
QuotaScanURL: webQuotaScanPath,
ConnectionsURL: webConnectionsPath,
StatusURL: webStatusPath,
FolderQuotaScanURL: webScanVFolderPath,
UsersTitle: pageUsersTitle,
AdminsTitle: pageAdminsTitle,
ConnectionsTitle: pageConnectionsTitle,
FoldersTitle: pageFoldersTitle,
StatusTitle: pageStatusTitle,
Version: version.GetAsString(),
LoggedAdmin: getAdminFromToken(r),
}
}
@ -191,16 +249,16 @@ func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
}
}
func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) {
func renderMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
var errorString string
if len(body) > 0 {
if body != "" {
errorString = body + " "
}
if err != nil {
errorString += err.Error()
}
data := messagePage{
basePage: getBasePageData(title, ""),
basePage: getBasePageData(title, "", r),
Error: errorString,
Success: message,
}
@ -208,22 +266,51 @@ func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int
renderTemplate(w, templateMessage, data)
}
func renderInternalServerErrorPage(w http.ResponseWriter, err error) {
renderMessagePage(w, page500Title, page500Body, http.StatusInternalServerError, err, "")
func renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
renderMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
}
func renderBadRequestPage(w http.ResponseWriter, err error) {
renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "")
func renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
renderMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
}
func renderNotFoundPage(w http.ResponseWriter, err error) {
renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "")
func renderForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
renderMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
}
func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
func renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
}
func renderChangePwdPage(w http.ResponseWriter, r *http.Request, error string) {
data := changePwdPage{
basePage: getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r),
Error: error,
}
renderTemplate(w, templateChangePwd, data)
}
func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
error string, isAdd bool) {
currentURL := webAdminPath
if !isAdd {
currentURL = fmt.Sprintf("%v/%v", webAdminPath, url.PathEscape(admin.Username))
}
data := adminPage{
basePage: getBasePageData("Add a new user", currentURL, r),
Admin: admin,
Error: error,
IsAdd: isAdd,
}
renderTemplate(w, templateAdmin, data)
}
func renderAddUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) {
user.SetEmptySecretsIfNil()
data := userPage{
basePage: getBasePageData("Add a new user", webUserPath),
basePage: getBasePageData("Add a new user", webUserPath, r),
IsAdd: true,
Error: error,
User: user,
@ -236,10 +323,10 @@ func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error stri
renderTemplate(w, templateUser, data)
}
func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
func renderUpdateUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) {
user.SetEmptySecretsIfNil()
data := userPage{
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username)), r),
IsAdd: false,
Error: error,
User: user,
@ -252,9 +339,9 @@ func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error s
renderTemplate(w, templateUser, data)
}
func renderAddFolderPage(w http.ResponseWriter, folder vfs.BaseVirtualFolder, error string) {
func renderAddFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder, error string) {
data := folderPage{
basePage: getBasePageData("Add a new folder", webFolderPath),
basePage: getBasePageData("Add a new folder", webFolderPath, r),
Error: error,
Folder: folder,
}
@ -571,6 +658,26 @@ func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, er
return fs, nil
}
func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
var admin dataprovider.Admin
err := r.ParseForm()
if err != nil {
return admin, err
}
status, err := strconv.Atoi(r.Form.Get("status"))
if err != nil {
return admin, err
}
admin.Username = r.Form.Get("username")
admin.Password = r.Form.Get("password")
admin.Permissions = r.Form["permissions"]
admin.Email = r.Form.Get("email")
admin.Status = status
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
admin.AdditionalInfo = r.Form.Get("additional_info")
return admin, nil
}
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
var user dataprovider.User
err := r.ParseMultipartForm(maxRequestSize)
@ -649,6 +756,152 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
return user, err
}
func renderLoginPage(w http.ResponseWriter, error string) {
data := loginPage{
CurrentURL: webLoginPath,
Version: version.Get().Version,
Error: error,
}
renderTemplate(w, templateLogin, data)
}
func handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {
renderChangePwdPage(w, r, "")
}
func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderChangePwdPage(w, r, err.Error())
return
}
err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
r.Form.Get("new_password2"))
if err != nil {
renderChangePwdPage(w, r, err.Error())
return
}
handleWebLogout(w, r)
}
func handleWebLogout(w http.ResponseWriter, r *http.Request) {
c := jwtTokenClaims{}
c.removeCookie(w)
http.Redirect(w, r, webLoginPath, http.StatusFound)
}
func handleWebLogin(w http.ResponseWriter, r *http.Request) {
renderLoginPage(w, "")
}
func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
limit := defaultQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok {
var err error
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
if err != nil {
limit = defaultQueryLimit
}
}
admins := make([]dataprovider.Admin, 0, limit)
for {
a, err := dataprovider.GetAdmins(limit, len(admins), dataprovider.OrderASC)
if err != nil {
renderInternalServerErrorPage(w, r, err)
return
}
admins = append(admins, a...)
if len(a) < limit {
break
}
}
data := adminsPage{
basePage: getBasePageData(pageAdminsTitle, webAdminsPath, r),
Admins: admins,
}
renderTemplate(w, templateAdmins, data)
}
func handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
admin := &dataprovider.Admin{Status: 1}
renderAddUpdateAdminPage(w, r, admin, "", true)
}
func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
username := getURLParam(r, "username")
admin, err := dataprovider.AdminExists(username)
if err == nil {
renderAddUpdateAdminPage(w, r, &admin, "", false)
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err)
} else {
renderInternalServerErrorPage(w, r, err)
}
}
func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
admin, err := getAdminFromPostFields(r)
if err != nil {
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
return
}
err = dataprovider.AddAdmin(&admin)
if err != nil {
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
return
}
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
}
func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
username := getURLParam(r, "username")
admin, err := dataprovider.AdminExists(username)
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err)
return
} else if err != nil {
renderInternalServerErrorPage(w, r, err)
return
}
updatedAdmin, err := getAdminFromPostFields(r)
if err != nil {
renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
return
}
updatedAdmin.ID = admin.ID
updatedAdmin.Username = admin.Username
if updatedAdmin.Password == "" {
updatedAdmin.Password = admin.Password
}
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
renderAddUpdateAdminPage(w, r, &updatedAdmin, fmt.Sprintf("Invalid token claims: %v", err), false)
return
}
if username == claims.Username {
if claims.isCriticalPermRemoved(updatedAdmin.Permissions) {
renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot remove these permissions to yourself", false)
return
}
if updatedAdmin.Status == 0 {
renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false)
return
}
}
err = dataprovider.UpdateAdmin(&updatedAdmin)
if err != nil {
renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
return
}
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
}
func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
limit := defaultQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok {
@ -660,9 +913,9 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
}
users := make([]dataprovider.User, 0, limit)
for {
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC, "")
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC)
if err != nil {
renderInternalServerErrorPage(w, err)
renderInternalServerErrorPage(w, r, err)
return
}
users = append(users, u...)
@ -671,52 +924,44 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
}
}
data := usersPage{
basePage: getBasePageData(pageUsersTitle, webUsersPath),
basePage: getBasePageData(pageUsersTitle, webUsersPath, r),
Users: users,
}
renderTemplate(w, templateUsers, data)
}
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("cloneFromId") != "" {
id, err := strconv.ParseInt(r.URL.Query().Get("cloneFromId"), 10, 64)
if err != nil {
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(id)
if r.URL.Query().Get("cloneFrom") != "" {
username := r.URL.Query().Get("cloneFrom")
user, err := dataprovider.UserExists(username)
if err == nil {
user.ID = 0
user.Username = ""
if err := user.DecryptSecrets(); err != nil {
renderInternalServerErrorPage(w, err)
renderInternalServerErrorPage(w, r, err)
return
}
renderAddUserPage(w, user, "")
renderAddUserPage(w, r, user, "")
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, err)
renderNotFoundPage(w, r, err)
} else {
renderInternalServerErrorPage(w, err)
renderInternalServerErrorPage(w, r, err)
}
} else {
user := dataprovider.User{Status: 1}
renderAddUserPage(w, user, "")
renderAddUserPage(w, r, user, "")
}
}
func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
if err != nil {
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(id)
username := getURLParam(r, "username")
user, err := dataprovider.UserExists(username)
if err == nil {
renderUpdateUserPage(w, user, "")
renderUpdateUserPage(w, r, user, "")
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, err)
renderNotFoundPage(w, r, err)
} else {
renderInternalServerErrorPage(w, err)
renderInternalServerErrorPage(w, r, err)
}
}
@ -724,40 +969,37 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
user, err := getUserFromPostFields(r)
if err != nil {
renderAddUserPage(w, user, err.Error())
renderAddUserPage(w, r, user, err.Error())
return
}
err = dataprovider.AddUser(&user)
if err == nil {
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
} else {
renderAddUserPage(w, user, err.Error())
renderAddUserPage(w, r, user, err.Error())
}
}
func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
if err != nil {
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(id)
username := getURLParam(r, "username")
user, err := dataprovider.UserExists(username)
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, err)
renderNotFoundPage(w, r, err)
return
} else if err != nil {
renderInternalServerErrorPage(w, err)
renderInternalServerErrorPage(w, r, err)
return
}
updatedUser, err := getUserFromPostFields(r)
if err != nil {
renderUpdateUserPage(w, user, err.Error())
renderUpdateUserPage(w, r, user, err.Error())
return
}
updatedUser.ID = user.ID
updatedUser.Username = user.Username
updatedUser.SetEmptySecretsIfNil()
if len(updatedUser.Password) == 0 {
if updatedUser.Password == "" {
updatedUser.Password = user.Password
}
updateEncryptedSecrets(&updatedUser, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
@ -771,13 +1013,13 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
}
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
} else {
renderUpdateUserPage(w, user, err.Error())
renderUpdateUserPage(w, r, user, err.Error())
}
}
func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
data := statusPage{
basePage: getBasePageData(pageStatusTitle, webStatusPath),
basePage: getBasePageData(pageStatusTitle, webStatusPath, r),
Status: getServicesStatus(),
}
renderTemplate(w, templateStatus, data)
@ -786,14 +1028,14 @@ func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
connectionStats := common.Connections.GetStats()
data := connectionsPage{
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath),
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
Connections: connectionStats,
}
renderTemplate(w, templateConnections, data)
}
func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
renderAddFolderPage(w, vfs.BaseVirtualFolder{}, "")
renderAddFolderPage(w, r, vfs.BaseVirtualFolder{}, "")
}
func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
@ -801,7 +1043,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
folder := vfs.BaseVirtualFolder{}
err := r.ParseForm()
if err != nil {
renderAddFolderPage(w, folder, err.Error())
renderAddFolderPage(w, r, folder, err.Error())
return
}
folder.MappedPath = r.Form.Get("mapped_path")
@ -810,7 +1052,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
if err == nil {
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
} else {
renderAddFolderPage(w, folder, err.Error())
renderAddFolderPage(w, r, folder, err.Error())
}
}
@ -827,7 +1069,7 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
for {
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, "")
if err != nil {
renderInternalServerErrorPage(w, err)
renderInternalServerErrorPage(w, r, err)
return
}
folders = append(folders, f...)
@ -837,7 +1079,7 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
}
data := foldersPage{
basePage: getBasePageData(pageFoldersTitle, webFoldersPath),
basePage: getBasePageData(pageFoldersTitle, webFoldersPath, r),
Folders: folders,
}
renderTemplate(w, templateFolders, data)

1246
httpdtest/httpdtest.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,6 @@ cd dist
BASE_DIR="../.."
cp ${BASE_DIR}/sftpgo.json .
cp ${BASE_DIR}/examples/rest-api-cli/sftpgo_api_cli .
sed -i "s|sftpgo.db|/var/lib/sftpgo/sftpgo.db|" sftpgo.json
sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" sftpgo.json
sed -i "s|\"templates\"|\"/usr/share/sftpgo/templates\"|" sftpgo.json
@ -61,9 +60,6 @@ contents:
- src: "${BASE_DIR}/init/sftpgo.service"
dst: "/lib/systemd/system/sftpgo.service"
- src: "./sftpgo_api_cli"
dst: "/usr/bin/sftpgo_api_cli"
- src: "${BASE_DIR}/templates/*"
dst: "/usr/share/sftpgo/templates/"
@ -84,9 +80,6 @@ overrides:
recommends:
- bash-completion
- mime-support
suggests:
- python3-requests
- python3-pygments
scripts:
postinstall: ../scripts/deb/postinstall.sh
preremove: ../scripts/deb/preremove.sh
@ -95,7 +88,6 @@ overrides:
recommends:
- bash-completion
- mailcap
# centos 8 has python3-requests, centos 6/7 python-requests
scripts:
postinstall: ../scripts/rpm/postinstall
preremove: ../scripts/rpm/preremove
@ -112,6 +104,5 @@ tar xvf nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz nfpm
chmod 755 nfpm
mkdir rpm
./nfpm -f nfpm.yaml pkg -p rpm -t rpm
sed -i "s|env python|env python3|" sftpgo_api_cli
mkdir deb
./nfpm -f nfpm.yaml pkg -p deb -t deb

View file

@ -1,5 +1,4 @@
sftpgo usr/bin
examples/rest-api-cli/sftpgo_api_cli usr/bin
sftpgo.json etc/sftpgo
init/sftpgo.service lib/systemd/system
bash_completion/sftpgo usr/share/bash-completion/completions

View file

@ -1,5 +1,4 @@
arm64/sftpgo usr/bin
examples/rest-api-cli/sftpgo_api_cli usr/bin
sftpgo.json etc/sftpgo
init/sftpgo.service lib/systemd/system
bash_completion/sftpgo usr/share/bash-completion/completions

View file

@ -1,5 +1,4 @@
ppc64le/sftpgo usr/bin
examples/rest-api-cli/sftpgo_api_cli usr/bin
sftpgo.json etc/sftpgo
init/sftpgo.service lib/systemd/system
bash_completion/sftpgo usr/share/bash-completion/completions

View file

@ -97,7 +97,7 @@ func (s *Service) Start() error {
providerConf := config.GetProviderConf()
err = dataprovider.Initialize(providerConf, s.ConfigDir)
err = dataprovider.Initialize(providerConf, s.ConfigDir, s.PortableMode == 0)
if err != nil {
logger.Error(logSender, "", "error initializing data provider: %v", err)
logger.ErrorToConsole("error initializing data provider: %v", err)

View file

@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/httpdtest"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/vfs"
)
@ -27,7 +27,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
usePubKey := false
u := getTestUserWithCryptFs(usePubKey)
u.QuotaSize = 6553600
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -56,7 +56,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
if assert.NoError(t, err) {
assert.Equal(t, encryptedFileSize, info.Size())
}
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
@ -73,7 +73,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
assert.NoError(t, err)
_, err = client.Lstat(testFileName)
assert.Error(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)
@ -82,7 +82,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -93,7 +93,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) {
usePubKey := false
u := getTestUserWithCryptFs(usePubKey)
u.QuotaSize = 6553600
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -113,7 +113,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) {
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -122,7 +122,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) {
func TestEmptyFile(t *testing.T) {
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -152,7 +152,7 @@ func TestEmptyFile(t *testing.T) {
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -162,7 +162,7 @@ func TestUploadResumeCryptFs(t *testing.T) {
// upload resume is not supported
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -183,7 +183,7 @@ func TestUploadResumeCryptFs(t *testing.T) {
assert.Contains(t, err.Error(), "SSH_FX_OP_UNSUPPORTED")
}
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -193,7 +193,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
usePubKey := false
u := getTestUserWithCryptFs(usePubKey)
u.QuotaFiles = 1000
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -213,7 +213,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
// now replace the same file, the quota must not change
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
@ -221,7 +221,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
// replacing a symlink is like uploading a new file
err = client.Symlink(testFileName, testFileName+".link")
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
@ -229,14 +229,14 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
expectedQuotaSize = expectedQuotaSize + encryptedFileSize
err = sftpUploadFile(testFilePath, testFileName+".link", testFileSize, client)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
}
// now set a quota size restriction and upload the same file, upload should fail for space limit exceeded
user.QuotaSize = encryptedFileSize*2 - 1
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -246,7 +246,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
err = client.Remove(testFileName)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
@ -256,7 +256,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
func TestQuotaScanCryptFs(t *testing.T) {
usePubKey := false
user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
assert.NoError(t, err)
testFileSize := int64(65535)
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
@ -274,25 +274,25 @@ func TestQuotaScanCryptFs(t *testing.T) {
err = os.Remove(testFilePath)
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
// create user with the same home dir, so there is at least an untracked file
user, _, err = httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
user, _, err = httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)
assert.NoError(t, err)
_, err = httpd.StartQuotaScan(user, http.StatusAccepted)
_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
assert.NoError(t, err)
assert.Eventually(t, func() bool {
scans, _, err := httpd.GetQuotaScans(http.StatusOK)
scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
if err == nil {
return len(scans) == 0
}
return false
}, 1*time.Second, 50*time.Millisecond)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -300,7 +300,7 @@ func TestQuotaScanCryptFs(t *testing.T) {
func TestGetMimeTypeCryptFs(t *testing.T) {
usePubKey := true
user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
assert.NoError(t, err)
client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -325,7 +325,7 @@ func TestGetMimeTypeCryptFs(t *testing.T) {
assert.Equal(t, "text/plain; charset=utf-8", mime)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -334,7 +334,7 @@ func TestGetMimeTypeCryptFs(t *testing.T) {
func TestTruncate(t *testing.T) {
// truncate is not supported
usePubKey := true
user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
assert.NoError(t, err)
client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
@ -352,7 +352,7 @@ func TestTruncate(t *testing.T) {
assert.Error(t, err)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -365,7 +365,7 @@ func TestSCPBasicHandlingCryptoFs(t *testing.T) {
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
u.QuotaSize = 6553600
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(131074)
@ -395,20 +395,20 @@ func TestSCPBasicHandlingCryptoFs(t *testing.T) {
}
err = os.Remove(localPath)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
// now overwrite the existing file
err = scpUpload(testFilePath, remoteUpPath, false, false)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -422,7 +422,7 @@ func TestSCPRecursiveCryptFs(t *testing.T) {
}
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
testBaseDirName := "atestdir"
testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)
@ -467,7 +467,7 @@ func TestSCPRecursiveCryptFs(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}

View file

@ -152,6 +152,7 @@ func (c *scpCommand) getUploadFileData(sizeToRead int64, transfer *transfer) err
}
if sizeToRead > 0 {
// we could replace this method with io.CopyN implementing "Write" method in transfer struct
remaining := sizeToRead
buf := make([]byte, int64(math.Min(32768, float64(sizeToRead))))
for {
@ -420,6 +421,7 @@ func (c *scpCommand) sendDownloadFileData(filePath string, stat os.FileInfo, tra
return err
}
// we could replace this method with io.CopyN implementing "Read" method in transfer struct
buf := make([]byte, 32768)
var n int
for {

File diff suppressed because it is too large Load diff

View file

@ -121,7 +121,6 @@
"sslmode": 0,
"connection_string": "",
"sql_tables_prefix": "",
"manage_users": 1,
"track_quota": 2,
"pool_size": 0,
"users_base_dir": "",
@ -153,7 +152,6 @@
"templates_path": "templates",
"static_files_path": "static",
"backups_path": "backups",
"auth_user_file": "",
"certificate_file": "",
"certificate_key_file": ""
},

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 108.3 108.3" style="enable-background:new 0 0 108.3 108.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#E6E6E6;}
.st1{fill:#FFB8B8;}
.st2{fill:#575A89;}
.st3{fill:#2F2E41;}
</style>
<g id="Group_45" transform="translate(-191 -152.079)">
<g id="Group_30" transform="translate(282.246 224.353)">
<path id="Path_944" class="st0" d="M17.1-18.1c0,10.5-3,20.8-8.8,29.6c-1.2,1.9-2.5,3.6-4,5.3c-3.4,4-7.3,7.4-11.6,10.3
c-1.2,0.8-2.4,1.5-3.6,2.2c-6.5,3.6-13.7,5.8-21,6.5c-1.7,0.2-3.4,0.2-5.1,0.2c-4.7,0-9.4-0.6-14-1.8c-2.6-0.7-5.1-1.6-7.6-2.6
c-1.3-0.5-2.5-1.1-3.7-1.8c-2.9-1.5-5.6-3.3-8.2-5.3c-1.2-0.9-2.3-1.9-3.4-2.9C-95.8,1.3-97.1-33-76.8-54.9s54.6-23.3,76.5-2.9
C10.8-47.6,17.1-33.2,17.1-18.1L17.1-18.1z"/>
<path id="Path_945" class="st1" d="M-50.2-13.2c0,0,4.9,13.7,1.1,21.4s6,16.4,6,16.4s25.8-13.1,22.5-19.7s-8.8-15.3-7.7-20.8
L-50.2-13.2z"/>
<ellipse id="Ellipse_185" class="st1" cx="-40.6" cy="-25.5" rx="17.5" ry="17.5"/>
<path id="Path_946" class="st2" d="M-51.1,34.2c-2.6-0.7-5.1-1.6-7.6-2.6l0.5-13.3l4.9-11c1.1,0.9,2.3,1.6,3.5,2.3
c0.3,0.2,0.6,0.3,0.9,0.5c4.6,2.2,12.2,4.2,19.5-1.3c2.7-2.1,5-4.7,6.7-7.6L-8.8,9l0.7,8.4l0.8,9.8c-1.2,0.8-2.4,1.5-3.6,2.2
c-6.5,3.6-13.7,5.8-21,6.5c-1.7,0.2-3.4,0.2-5.1,0.2C-41.8,36.1-46.5,35.4-51.1,34.2z"/>
<path id="Path_947" class="st2" d="M-47.7-0.9L-47.7-0.9l-0.7,7.2l-0.4,3.8l-0.5,5.6l-1.8,18.5c-2.6-0.7-5.1-1.6-7.6-2.6
c-1.3-0.5-2.5-1.1-3.7-1.8c-2.9-1.5-5.6-3.3-8.2-5.3l-1.9-9l0.1-0.1L-47.7-0.9z"/>
<path id="Path_948" class="st2" d="M-10.9,29.3c-6.5,3.6-13.7,5.8-21,6.5c0.4-6.7,1-13.1,1.6-18.8c0.3-2.9,0.7-5.7,1.1-8.2
c1.2-8,2.5-13.5,3.4-14.2l6.1,4L4.9,7.3l-0.5,9.5c-3.4,4-7.3,7.4-11.6,10.3C-8.5,27.9-9.7,28.7-10.9,29.3z"/>
<path id="Path_949" class="st2" d="M-70.5,24.6c-1.2-0.9-2.3-1.9-3.4-2.9l0.9-6.1l0.7-0.1l3.1-0.4l6.8,14.8
C-65.2,28.3-67.9,26.6-70.5,24.6L-70.5,24.6z"/>
<path id="Path_950" class="st2" d="M8.3,11.5c-1.2,1.9-2.5,3.6-4,5.3c-3.4,4-7.3,7.4-11.6,10.3c-1.2,0.8-2.4,1.5-3.6,2.2l-0.6-2.8
l3.5-9.1l4.2-11.1l8.8,1.1C6.1,8.7,7.2,10.1,8.3,11.5z"/>
<path id="Path_951" class="st3" d="M-23.9-41.4c-2.7-4.3-6.8-7.5-11.6-8.9l-3.6,2.9l1.4-3.3c-1.2-0.2-2.3-0.2-3.5-0.2l-3.2,4.1
l1.3-4c-5.6,0.7-10.7,3.7-14,8.3c-4.1,5.9-4.8,14.1-0.8,20c1.1-3.4,2.4-6.6,3.5-9.9c0.9,0.1,1.7,0.1,2.6,0l1.3-3.1l0.4,3
c4.2-0.4,10.3-1.2,14.3-1.9l-0.4-2.3l2.3,1.9c1.2-0.3,1.9-0.5,1.9-0.7c2.9,4.7,5.8,7.7,8.8,12.5C-22.1-29.8-20.2-35.3-23.9-41.4z"
/>
<ellipse id="Ellipse_186" class="st1" cx="-24.9" cy="-26.1" rx="1.2" ry="2.4"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
/*!
* Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M12 192h424c6.6 0 12 5.4 12 12v260c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V204c0-6.6 5.4-12 12-12zm436-44v-36c0-26.5-21.5-48-48-48h-48V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H160V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H48C21.5 64 0 85.5 0 112v36c0 6.6 5.4 12 12 12h424c6.6 0 12-5.4 12-12z"/></svg>

Before

Width:  |  Height:  |  Size: 392 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z"/></svg>

Before

Width:  |  Height:  |  Size: 475 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"/></svg>

Before

Width:  |  Height:  |  Size: 402 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M464 128H272l-64-64H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V176c0-26.51-21.49-48-48-48z"/></svg>

Before

Width:  |  Height:  |  Size: 207 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/></svg>

Before

Width:  |  Height:  |  Size: 659 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/></svg>

Before

Width:  |  Height:  |  Size: 336 B

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 876 KiB

After

Width:  |  Height:  |  Size: 896 KiB

92
templates/admin.html Normal file
View file

@ -0,0 +1,92 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<!-- Page Heading -->
<h1 class="h5 mb-4 text-gray-800">{{if .IsAdd}}Add a new admin{{else}}Edit admin{{end}}</h1>
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="admin_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idUsername" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idUsername" name="username" placeholder=""
value="{{.Admin.Username}}" maxlength="255" autocomplete="nope" required
{{if not .IsAdd}}readonly{{end}}>
</div>
</div>
<div class="form-group row">
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
<div class="col-sm-10">
<select class="form-control" id="idStatus" name="status">
<option value="1" {{if eq .Admin.Status 1 }}selected{{end}}>Active</option>
<option value="0" {{if eq .Admin.Status 0 }}selected{{end}}>Inactive</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idPassword" name="password" placeholder=""
{{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
{{if not .IsAdd}}
<small id="pwdHelpBlock" class="form-text text-muted">
If empty the current password will not be changed
</small>
{{end}}
</div>
</div>
<div class="form-group row">
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
<div class="col-sm-10">
<select class="form-control" id="idPermissions" name="permissions" required multiple>
{{range $validPerm := .Admin.GetValidPerms}}
<option value="{{$validPerm}}"
{{range $perm := $.Admin.Permissions }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}
</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
value="{{.Admin.Email}}" maxlength="255">
</div>
</div>
<div class="form-group row">
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idAllowedIP" name="allowed_ip" placeholder=""
value="{{.Admin.GetAllowedIPAsString}}" maxlength="255" aria-describedby="allowedIPHelpBlock">
<small id="allowedIPHelpBlock" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
</div>
<div class="form-group row">
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
<div class="col-sm-10">
<textarea class="form-control" id="idAdditionalInfo" name="additional_info" rows="3"
aria-describedby="additionalInfoHelpBlock">{{.Admin.AdditionalInfo}}</textarea>
<small id="additionalInfoHelpBlock" class="form-text text-muted">
Free form text field
</small>
</div>
</div>
<button type="submit" class="btn btn-primary float-right mt-3 mb-5 px-5 px-3">Submit</button>
</form>
{{end}}

185
templates/admins.html Normal file
View file

@ -0,0 +1,185 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="/static/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="/static/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
<link href="/static/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="errorTxt" class="card-body text-form-error"></div>
</div>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage admins</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Status</th>
<th>Permissions</th>
<th>Other</th>
</tr>
</thead>
<tbody>
{{range .Admins}}
<tr>
<td>{{.ID}}</td>
<td>{{.Username}}</td>
<td>{{if eq .Status 1 }}Active{{else}}Inactive{{end}}</td>
<td>{{.GetPermissionsAsString}}</td>
<td>{{.GetInfoString}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected admin?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="/static/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="/static/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="/static/vendor/datatables/dataTables.select.min.js"></script>
<script src="/static/vendor/datatables/select.bootstrap4.min.js"></script>
<script src="/static/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="/static/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script type="text/javascript">
function deleteAction() {
var table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
var username = table.row({ selected: true }).data()[1];
var path = '{{.AdminURL}}' + "/" + username;
$('#deleteModal').modal('hide');
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
timeout: 15000,
success: function (result) {
table.button('delete:name').enable(true);
window.location.href = '{{.AdminsURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
table.button('delete:name').enable(true);
var txt = "Unable to delete the selected admin";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
txt += ": " + json.error;
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
setTimeout(function () {
$('#errorMsg').hide();
}, 5000);
}
});
}
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: 'Add',
name: 'add',
action: function (e, dt, node, config) {
window.location.href = '{{.AdminURL}}';
}
};
$.fn.dataTable.ext.buttons.edit = {
text: 'Edit',
name: 'edit',
action: function (e, dt, node, config) {
var username = dt.row({ selected: true }).data()[1];
var path = '{{.AdminURL}}' + "/" + username;
window.location.href = encodeURI(path);
},
enabled: false
};
$.fn.dataTable.ext.buttons.delete = {
text: 'Delete',
name: 'delete',
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
var table = $('#dataTable').DataTable({
dom: "<'row'<'col-sm-12'B>>" +
"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
select: true,
buttons: [],
"columnDefs": [
{
"targets": [0],
"visible": false,
"searchable": false
},
],
"scrollX": false,
"order": [[1, 'asc']]
});
{{if .LoggedAdmin.HasPermission "manage_admins"}}
table.button().add(0,'delete');
table.button().add(0,'edit');
table.button().add(0,'add');
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button('edit:name').enable(selectedRows == 1);
table.button('delete:name').enable(selectedRows == 1);
});
{{end}}
});
</script>
{{end}}

View file

@ -15,7 +15,8 @@
<link rel="shortcut icon" href="/static/favicon.ico" />
<!-- Custom fonts for this template-->
<link href="/static/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link href="/static/vendor/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css">
<link href="/static/vendor/fontawesome-free/css/solid.min.css" rel="stylesheet" type="text/css">
<link href="/static/css/fonts.css" rel="stylesheet">
<!-- Custom styles for this template-->
@ -38,6 +39,7 @@
<!-- Page Wrapper -->
<div id="wrapper">
{{if .LoggedAdmin.Username}}
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
@ -52,10 +54,10 @@
<!-- Divider -->
<hr class="sidebar-divider my-0">
{{ if .LoggedAdmin.HasPermission "view_users"}}
<li class="nav-item {{if eq .CurrentURL .UsersURL}}active{{end}}">
<a class="nav-link" href="{{.UsersURL}}">
<i class="fas fa-fw fa-user"></i>
<i class="fas fa-users"></i>
<span>{{.UsersTitle}}</span></a>
</li>
@ -64,18 +66,31 @@
<i class="fas fa-folder"></i>
<span>{{.FoldersTitle}}</span></a>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "view_conns"}}
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
<a class="nav-link" href="{{.ConnectionsURL}}">
<i class="fas fa-exchange-alt"></i>
<span>{{.ConnectionsTitle}}</span></a>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "manage_admins"}}
<li class="nav-item {{if eq .CurrentURL .AdminsURL}}active{{end}}">
<a class="nav-link" href="{{.AdminsURL}}">
<i class="fas fa-user-cog"></i>
<span>{{.AdminsTitle}}</span></a>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "view_status"}}
<li class="nav-item {{if eq .CurrentURL .StatusURL}}active{{end}}">
<a class="nav-link" href="{{.StatusURL}}">
<i class="fas fa-info-circle"></i>
<span>{{.StatusTitle}}</span></a>
</li>
{{end}}
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
@ -87,6 +102,7 @@
</ul>
<!-- End of Sidebar -->
{{end}}
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
@ -94,11 +110,43 @@
<!-- Main Content -->
<div id="content">
{{if .LoggedAdmin.Username}}
<!-- Topbar -->
<nav class="mb-4 static-top">
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">{{.LoggedAdmin.Username}}</span>
<img class="img-profile rounded-circle" src="/static/img/undraw_profile.svg">
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
<a class="dropdown-item" href="{{.ChangeAdminPwdURL}}">
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
Change password
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
{{end}}
<!-- Begin Page Content -->
<div class="container-fluid">
@ -110,7 +158,7 @@
</div>
<!-- End of Main Content -->
{{if .LoggedAdmin.Username}}
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
@ -120,6 +168,7 @@
</div>
</footer>
<!-- End of Footer -->
{{end}}
</div>
<!-- End of Content Wrapper -->
@ -132,6 +181,26 @@
<i class="fas fa-angle-up"></i>
</a>
<!-- Logout Modal-->
<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="{{.LogoutURL}}">Logout</a>
</div>
</div>
</div>
</div>
{{block "dialog" .}}{{end}}
<!-- Bootstrap core JavaScript-->

38
templates/changepwd.html Normal file
View file

@ -0,0 +1,38 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<!-- Page Heading -->
<h1 class="h5 mb-4 text-gray-800">Change password</h1>
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="user_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" required>
</div>
</div>
<button type="submit" class="btn btn-primary float-right mt-3 mb-5 px-5 px-3">Change my password</button>
</form>
{{end}}

View file

@ -90,9 +90,9 @@
function disconnectAction() {
var table = $('#dataTable').DataTable();
table.button(0).enable(false);
table.button('disconnect:name').enable(false);
var connectionID = table.row({ selected: true }).data()[0];
var path = '{{.APIConnectionsURL}}' + "/" + connectionID;
var path = '{{.ConnectionsURL}}' + "/" + connectionID;
$('#disconnectModal').modal('hide');
$.ajax({
url: path,
@ -101,12 +101,12 @@
timeout: 15000,
success: function (result) {
setTimeout(function () {
table.button(0).enable(true);
table.button('disconnect:name').enable(true);
window.location.href = '{{.ConnectionsURL}}';
}, 1000);
},
error: function ($xhr, textStatus, errorThrown) {
table.button(0).enable(true);
table.button('disconnect:name').enable(true);
var txt = "Unable to close the selected connection";
if ($xhr) {
var json = $xhr.responseJSON;
@ -126,6 +126,7 @@
$(document).ready(function () {
$.fn.dataTable.ext.buttons.disconnect = {
text: 'Disconnect',
name: 'disconnect',
action: function (e, dt, node, config) {
$('#disconnectModal').modal('show');
},
@ -138,9 +139,7 @@
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
select: true,
buttons: [
'disconnect'
],
buttons: [],
"columnDefs": [
{
"targets": [0],
@ -152,10 +151,14 @@
"order": [[1, 'asc']]
});
{{if .LoggedAdmin.HasPermission "close_conns"}}
table.button().add(0,'disconnect');
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button(0).enable(selectedRows == 1);
table.button('disconnect:name').enable(selectedRows == 1);
});
{{end}}
});
</script>
{{end}}

View file

@ -87,9 +87,9 @@
function deleteAction() {
var table = $('#dataTable').DataTable();
table.button(1).enable(false);
table.button('delete:name').enable(false);
var folderPath = table.row({ selected: true }).data()[0];
var path = '{{.APIFoldersURL}}' + "?folder_path=" + encodeURIComponent(folderPath);
var path = '{{.FolderURL}}' + "?folder_path=" + encodeURIComponent(folderPath);
$('#deleteModal').modal('hide');
$.ajax({
url: path,
@ -97,12 +97,11 @@ function deleteAction() {
dataType: 'json',
timeout: 15000,
success: function (result) {
table.button(1).enable(true);
table.button('delete:name').enable(true);
window.location.href = '{{.FoldersURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
console.log("delete error")
table.button(1).enable(true);
table.button('delete:name').enable(true);
var txt = "Unable to delete the selected folder";
if ($xhr) {
var json = $xhr.responseJSON;
@ -122,6 +121,7 @@ function deleteAction() {
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: 'Add',
name: 'add',
action: function (e, dt, node, config) {
window.location.href = '{{.FolderURL}}';
}
@ -129,6 +129,7 @@ function deleteAction() {
$.fn.dataTable.ext.buttons.delete = {
text: 'Delete',
name: 'delete',
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
@ -137,10 +138,11 @@ function deleteAction() {
$.fn.dataTable.ext.buttons.quota_scan = {
text: 'Quota scan',
name: 'quota_scan',
action: function (e, dt, node, config) {
table.button(2).enable(false);
dt.button('quota_scan:name').enable(false);
var folderPath = dt.row({ selected: true }).data()[0];
var path = '{{.APIFolderQuotaScanURL}}'
var path = '{{.FolderQuotaScanURL}}'
$.ajax({
url: path,
type: 'POST',
@ -148,7 +150,7 @@ function deleteAction() {
data: JSON.stringify({ "mapped_path": folderPath }),
timeout: 15000,
success: function (result) {
table.button(2).enable(true);
dt.button('quota_scan:name').enable(true);
$('#successTxt').text("Quota scan started for the selected folder. Please reload the folders page to check when the scan ends");
$('#successMsg').show();
setTimeout(function () {
@ -156,8 +158,7 @@ function deleteAction() {
}, 5000);
},
error: function ($xhr, textStatus, errorThrown) {
console.log("quota scan error")
table.button(2).enable(true);
dt.button('quota_scan:name').enable(true);
var txt = "Unable to update quota for the selected folder";
if ($xhr) {
var json = $xhr.responseJSON;
@ -186,17 +187,31 @@ function deleteAction() {
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
select: true,
buttons: [
'add','delete', 'quota_scan'
],
buttons: [],
"scrollX": false,
"order": [[0, 'asc']]
});
{{if .LoggedAdmin.HasPermission "quota_scans"}}
table.button().add(0,'quota_scan');
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button().add(0,'delete');
{{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button().add(0,'add');
{{end}}
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button(1).enable(selectedRows == 1);
table.button(2).enable(selectedRows == 1);
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button('delete:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "quota_scans"}}
table.button('quota_scan:name').enable(selectedRows == 1);
{{end}}
});
});

109
templates/login.html Normal file
View file

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>SFTPGo - Login</title>
<link rel="shortcut icon" href="/static/favicon.ico" />
<!-- Custom fonts for this template-->
<link href="/static/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link href="/static/css/fonts.css" rel="stylesheet">
<!-- Custom styles for this template-->
<link href="/static/css/sb-admin-2.min.css" rel="stylesheet">
<style>
div.dt-buttons {
margin-bottom: 1em;
}
.text-form-error {
color: var(--red) !important;
}
form.user-custom .custom-checkbox.small label {
line-height: 1.5rem;
}
form.user-custom .form-control-user-custom {
font-size: 0.9rem;
border-radius: 10rem;
padding: 1.5rem 1rem;
}
form.user-custom .btn-user-custom {
font-size: 0.9rem;
border-radius: 10rem;
padding: 0.75rem 1rem;
}
</style>
</head>
<body class="bg-gradient-primary">
<div class="container">
<!-- Outer Row -->
<div class="row justify-content-center">
<div class="col-xl-6 col-lg-7 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-12">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">SFTPGo - {{.Version}}</h1>
</div>
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
class="user-custom">
<div class="form-group">
<input type="text" class="form-control form-control-user-custom"
id="inputUsername" name="username" placeholder="Username">
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user-custom"
id="inputPassword" name="password" placeholder="Password">
</div>
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
Login
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="/static/vendor/jquery/jquery.min.js"></script>
<script src="/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="/static/vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="/static/js/sb-admin-2.min.js"></script>
</body>
</html>

View file

@ -3,6 +3,7 @@
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
{{if .LoggedAdmin.Username}}
<h1 class="h5 mb-4 text-gray-800">{{.Title}}</h1>
{{if .Error}}
<div class="card mb-4 border-left-warning">
@ -15,5 +16,36 @@
<div class="card-body">{{.Success}}</div>
</div>
{{end}}
{{else}}
<div class="row justify-content-center">
<div class="col-xl-8 col-lg-9 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<div class="row justify-content-center">
<div class="col-lg-9">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">{{.Title}}</h1>
</div>
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
{{if .Success}}
<div class="card mb-4 border-left-success">
<div class="card-body">{{.Success}}</div>
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{end}}
{{end}}

View file

@ -50,7 +50,7 @@
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idPassword" name="password" placeholder=""
autocomplete="new-password" {{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
{{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
{{if not .IsAdd}}
<small id="pwdHelpBlock" class="form-text text-muted">
If empty the current password will not be changed

View file

@ -97,22 +97,21 @@
function deleteAction() {
var table = $('#dataTable').DataTable();
table.button(3).enable(false);
var userID = table.row({ selected: true }).data()[0];
var path = '{{.APIUserURL}}' + "/" + userID;
table.button('delete:name').enable(false);
var username = table.row({ selected: true }).data()[1];
var path = '{{.UserURL}}' + "/" + username;
$('#deleteModal').modal('hide');
$.ajax({
url: path,
url: encodeURI(path),
type: 'DELETE',
dataType: 'json',
timeout: 15000,
success: function (result) {
table.button(3).enable(true);
table.button('delete:name').enable(true);
window.location.href = '{{.UsersURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
console.log("delete error")
table.button(3).enable(true);
table.button('delete:name').enable(true);
var txt = "Unable to delete the selected user";
if ($xhr) {
var json = $xhr.responseJSON;
@ -132,6 +131,7 @@
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: 'Add',
name: 'add',
action: function (e, dt, node, config) {
window.location.href = '{{.UserURL}}';
}
@ -139,19 +139,21 @@
$.fn.dataTable.ext.buttons.edit = {
text: 'Edit',
name: 'edit',
action: function (e, dt, node, config) {
var userID = dt.row({ selected: true }).data()[0];
var path = '{{.UserURL}}' + "/" + userID;
window.location.href = path;
var username = dt.row({ selected: true }).data()[1];
var path = '{{.UserURL}}' + "/" + username;
window.location.href = encodeURI(path);
},
enabled: false
};
$.fn.dataTable.ext.buttons.clone = {
text: 'Clone',
name: 'clone',
action: function (e, dt, node, config) {
var userID = dt.row({ selected: true }).data()[0];
var path = '{{.UserURL}}' + "?cloneFromId=" + userID;
var username = dt.row({ selected: true }).data()[1];
var path = '{{.UserURL}}' + "?cloneFrom=" + encodeURIComponent(username);
window.location.href = path;
},
enabled: false
@ -159,6 +161,7 @@
$.fn.dataTable.ext.buttons.delete = {
text: 'Delete',
name: 'delete',
action: function (e, dt, node, config) {
/*console.log("delete clicked, num row selected: " + dt.rows({ selected: true }).count());
var data = dt.rows({ selected: true }).data();
@ -172,10 +175,11 @@
$.fn.dataTable.ext.buttons.quota_scan = {
text: 'Quota scan',
name: 'quota_scan',
action: function (e, dt, node, config) {
table.button(4).enable(false);
dt.button('quota_scan:name').enable(false);
var username = dt.row({ selected: true }).data()[1];
var path = '{{.APIQuotaScanURL}}'
var path = '{{.QuotaScanURL}}'
$.ajax({
url: path,
type: 'POST',
@ -183,7 +187,7 @@
data: JSON.stringify({ "username": username }),
timeout: 15000,
success: function (result) {
table.button(4).enable(true);
dt.button('quota_scan:name').enable(true);
$('#successTxt').text("Quota scan started for the selected user. Please reload the user's page to check when the scan ends");
$('#successMsg').show();
setTimeout(function () {
@ -191,8 +195,7 @@
}, 5000);
},
error: function ($xhr, textStatus, errorThrown) {
console.log("quota scan error")
table.button(4).enable(true);
dt.button('quota_scan:name').enable(true);
var txt = "Unable to update quota for the selected user";
if ($xhr) {
var json = $xhr.responseJSON;
@ -221,9 +224,7 @@
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
select: true,
buttons: [
'add', 'edit', 'clone', 'delete', 'quota_scan'
],
buttons: [],
"columnDefs": [
{
"targets": [0],
@ -235,12 +236,41 @@
"order": [[1, 'asc']]
});
{{if .LoggedAdmin.HasPermission "quota_scans"}}
table.button().add(0,'quota_scan');
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button().add(0,'delete');
{{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button().add(0,'clone');
{{end}}
{{if .LoggedAdmin.HasPermission "edit_users"}}
table.button().add(0,'edit');
{{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button().add(0,'add');
{{end}}
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button(1).enable(selectedRows == 1);
table.button(2).enable(selectedRows == 1);
table.button(3).enable(selectedRows == 1);
table.button(4).enable(selectedRows == 1);
{{if .LoggedAdmin.HasPermission "edit_users"}}
table.button('edit:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button('clone:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button('delete:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "quota_scans"}}
table.button('quota_scan:name').enable(selectedRows == 1);
{{end}}
});
});
</script>

View file

@ -26,12 +26,15 @@ import (
"strings"
"time"
"github.com/rs/xid"
"golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/logger"
)
const logSender = "utils"
const (
logSender = "utils"
)
// IsStringInSlice searches a string in a slice and returns true if the string is found
func IsStringInSlice(obj string, list []string) bool {
@ -383,6 +386,22 @@ func createDirPathIfMissing(file string, perm os.FileMode) error {
return nil
}
// GenerateRandomBytes generates the secret to use for JWT auth
func GenerateRandomBytes(length int) []byte {
b := make([]byte, length)
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
return b
}
b = xid.New().Bytes()
for len(b) < length {
b = append(b, xid.New().Bytes()...)
}
return b[:length]
}
// HTTPListenAndServe is a wrapper for ListenAndServe that support both tcp
// and Unix-domain sockets
func HTTPListenAndServe(srv *http.Server, address string, port int, isTLS bool, logSender string) error {

View file

@ -980,7 +980,7 @@ func TestBasicUsersCache(t *testing.T) {
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.True(t, ok)
// cache is invalidated after user deletion
err = dataprovider.DeleteUser(&user)
err = dataprovider.DeleteUser(user.Username)
assert.NoError(t, err)
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.False(t, ok)
@ -1164,13 +1164,13 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
assert.True(t, ok)
err = dataprovider.DeleteUser(&user1)
err = dataprovider.DeleteUser(user1.Username)
assert.NoError(t, err)
err = dataprovider.DeleteUser(&user2)
err = dataprovider.DeleteUser(user2.Username)
assert.NoError(t, err)
err = dataprovider.DeleteUser(&user3)
err = dataprovider.DeleteUser(user3.Username)
assert.NoError(t, err)
err = dataprovider.DeleteUser(&user4)
err = dataprovider.DeleteUser(user4.Username)
assert.NoError(t, err)
err = os.RemoveAll(u.GetHomeDir())

View file

@ -29,7 +29,7 @@ import (
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/httpclient"
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/httpdtest"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
@ -127,7 +127,7 @@ func TestMain(m *testing.M) {
os.Exit(1)
}
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
if err != nil {
logger.ErrorToConsole("error initializing data provider: %v", err)
os.Exit(1)
@ -144,7 +144,7 @@ func TestMain(m *testing.M) {
httpdConf := config.GetHTTPDConfig()
httpdConf.BindPort = 8078
httpd.SetBaseURLAndCredentials("http://127.0.0.1:8078", "", "")
httpdtest.SetBaseURL("http://127.0.0.1:8078")
// required to test sftpfs
sftpdConf := config.GetSFTPDConfig()
@ -288,11 +288,11 @@ func TestInitialization(t *testing.T) {
func TestBasicHandling(t *testing.T) {
u := getTestUser()
u.QuotaSize = 6553600
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.QuotaSize = 6553600
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client := getWebDavClient(user)
@ -311,7 +311,7 @@ func TestBasicHandling(t *testing.T) {
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
@ -322,13 +322,13 @@ func TestBasicHandling(t *testing.T) {
// the webdav client hide the error we check the quota
err = client.Remove(testFileName)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
err = client.Remove(testFileName + "1")
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
@ -364,9 +364,9 @@ func TestBasicHandling(t *testing.T) {
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -378,7 +378,7 @@ func TestBasicHandling(t *testing.T) {
func TestBasicHandlingCryptFs(t *testing.T) {
u := getTestUserWithCryptFs()
u.QuotaSize = 6553600
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
@ -399,7 +399,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
@ -410,7 +410,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
}
err = client.Remove(testFileName)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)
@ -443,7 +443,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -453,13 +453,13 @@ func TestBasicHandlingCryptFs(t *testing.T) {
func TestPropPatch(t *testing.T) {
u := getTestUser()
u.Username = u.Username + "1"
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser := getTestSFTPUser()
sftpUser.FsConfig.SFTPConfig.Username = localUser.Username
for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} {
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client), sftpUser.Username)
@ -486,13 +486,13 @@ func TestPropPatch(t *testing.T) {
}
err = os.Remove(testFilePath)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
}
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -500,14 +500,14 @@ func TestPropPatch(t *testing.T) {
func TestLoginInvalidPwd(t *testing.T) {
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
user.Password = "wrong"
client = getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
@ -527,7 +527,7 @@ func TestDefender(t *testing.T) {
err := common.Initialize(cfg)
assert.NoError(t, err)
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
@ -545,7 +545,7 @@ func TestDefender(t *testing.T) {
assert.Contains(t, err.Error(), "403")
}
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -556,26 +556,26 @@ func TestDefender(t *testing.T) {
func TestLoginInvalidURL(t *testing.T) {
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u1 := getTestUser()
u1.Username = user.Username + "1"
user1, _, err := httpd.AddUser(u1, http.StatusOK)
user1, _, err := httpdtest.AddUser(u1, http.StatusCreated)
assert.NoError(t, err)
rootPath := fmt.Sprintf("http://%v/%v", webDavServerAddr, user.Username+"1")
client := gowebdav.NewClient(rootPath, user.Username, defaultPassword)
client.SetTimeout(5 * time.Second)
assert.Error(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user1, http.StatusOK)
_, err = httpdtest.RemoveUser(user1, http.StatusOK)
assert.NoError(t, err)
}
func TestRootRedirect(t *testing.T) {
errRedirect := errors.New("redirect error")
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
@ -612,7 +612,7 @@ func TestRootRedirect(t *testing.T) {
err = resp.Body.Close()
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
@ -630,29 +630,26 @@ func TestLoginExternalAuth(t *testing.T) {
assert.NoError(t, err)
providerConf.ExternalAuthHook = extAuthPath
providerConf.ExternalAuthScope = 0
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
client := getWebDavClient(u)
assert.NoError(t, checkBasicFunc(client))
u.Username = defaultUsername + "1"
client = getWebDavClient(u)
assert.Error(t, checkBasicFunc(client))
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, defaultUsername, user.Username)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
if assert.Len(t, users, 1) {
user := users[0]
assert.Equal(t, defaultUsername, user.Username)
_, err = httpd.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
err = dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = os.Remove(extAuthPath)
assert.NoError(t, err)
@ -671,30 +668,27 @@ func TestPreLoginHook(t *testing.T) {
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)
assert.NoError(t, err)
providerConf.PreLoginHook = preLoginPath
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)
assert.NoError(t, err)
assert.Equal(t, 0, len(users))
client := getWebDavClient(u)
assert.NoError(t, checkBasicFunc(client))
users, _, err = httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 1, len(users))
user := users[0]
// test login with an existing user
client = getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)
assert.NoError(t, err)
// update the user to remove it from the cache
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
// update the user to remove it from the cache
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
user.Status = 0
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)
@ -702,7 +696,7 @@ func TestPreLoginHook(t *testing.T) {
client = getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -711,7 +705,7 @@ func TestPreLoginHook(t *testing.T) {
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = os.Remove(preLoginPath)
assert.NoError(t, err)
@ -724,7 +718,7 @@ func TestPostConnectHook(t *testing.T) {
common.Config.PostConnectHook = postConnectPath
u := getTestUser()
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
err = ioutil.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
assert.NoError(t, err)
@ -734,13 +728,13 @@ func TestPostConnectHook(t *testing.T) {
assert.NoError(t, err)
assert.Error(t, checkBasicFunc(client))
common.Config.PostConnectHook = "http://127.0.0.1:8078/api/v1/version"
common.Config.PostConnectHook = "http://127.0.0.1:8078/healthz"
assert.NoError(t, checkBasicFunc(client))
common.Config.PostConnectHook = "http://127.0.0.1:8078/notfound"
assert.Error(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -752,7 +746,7 @@ func TestMaxConnections(t *testing.T) {
oldValue := common.Config.MaxTotalConnections
common.Config.MaxTotalConnections = 1
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
@ -764,7 +758,7 @@ func TestMaxConnections(t *testing.T) {
common.Connections.Add(connection)
assert.Error(t, checkBasicFunc(client))
common.Connections.Remove(connection.GetID())
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -776,7 +770,7 @@ func TestMaxConnections(t *testing.T) {
func TestMaxSessions(t *testing.T) {
u := getTestUser()
u.MaxSessions = 1
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
@ -788,7 +782,7 @@ func TestMaxSessions(t *testing.T) {
common.Connections.Add(connection)
assert.Error(t, checkBasicFunc(client))
common.Connections.Remove(connection.GetID())
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -799,12 +793,12 @@ func TestLoginWithIPilters(t *testing.T) {
u := getTestUser()
u.Filters.DeniedIP = []string{"192.167.0.0/24", "172.18.0.0/16"}
u.Filters.AllowedIP = []string{"172.19.0.0/16"}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -833,7 +827,7 @@ func TestDownloadErrors(t *testing.T) {
DeniedPatterns: []string{"*.jpg"},
},
}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
testFilePath1 := filepath.Join(user.HomeDir, subDir1, "file.zipp")
@ -861,7 +855,7 @@ func TestDownloadErrors(t *testing.T) {
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -883,7 +877,7 @@ func TestUploadErrors(t *testing.T) {
DeniedExtensions: []string{".zip"},
},
}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
testFilePath := filepath.Join(homeBasePath, testFileName)
@ -918,7 +912,7 @@ func TestUploadErrors(t *testing.T) {
err = os.Remove(testFilePath)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -927,18 +921,18 @@ func TestUploadErrors(t *testing.T) {
func TestDeniedLoginMethod(t *testing.T) {
u := getTestUser()
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndKeyboardInt}
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -947,18 +941,18 @@ func TestDeniedLoginMethod(t *testing.T) {
func TestDeniedProtocols(t *testing.T) {
u := getTestUser()
u.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolFTP}
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
assert.NoError(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -967,11 +961,11 @@ func TestDeniedProtocols(t *testing.T) {
func TestQuotaLimits(t *testing.T) {
u := getTestUser()
u.QuotaFiles = 1
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.QuotaFiles = 1
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
testFileSize := int64(65535)
@ -1002,7 +996,7 @@ func TestQuotaLimits(t *testing.T) {
// test quota size
user.QuotaSize = testFileSize - 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = uploadFile(testFilePath, testFileName+".quota", testFileSize, client)
assert.Error(t, err)
@ -1011,7 +1005,7 @@ func TestQuotaLimits(t *testing.T) {
// now test quota limits while uploading the current file, we have 1 bytes remaining
user.QuotaSize = testFileSize + 1
user.QuotaFiles = 0
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = uploadFile(testFilePath1, testFileName1, testFileSize1, client)
assert.Error(t, err)
@ -1040,13 +1034,13 @@ func TestQuotaLimits(t *testing.T) {
assert.NoError(t, err)
user.QuotaFiles = 0
user.QuotaSize = 0
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1056,11 +1050,11 @@ func TestUploadMaxSize(t *testing.T) {
testFileSize := int64(65535)
u := getTestUser()
u.Filters.MaxUploadFileSize = testFileSize + 1
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.Filters.MaxUploadFileSize = testFileSize + 1
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
testFilePath := filepath.Join(homeBasePath, testFileName)
@ -1090,13 +1084,13 @@ func TestUploadMaxSize(t *testing.T) {
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
user.Filters.MaxUploadFileSize = 65536000
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1106,12 +1100,12 @@ func TestClientClose(t *testing.T) {
u := getTestUser()
u.UploadBandwidth = 64
u.DownloadBandwidth = 64
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.UploadBandwidth = 64
u.DownloadBandwidth = 64
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
testFileSize := int64(1048576)
@ -1179,9 +1173,9 @@ func TestClientClose(t *testing.T) {
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1202,7 +1196,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoError(t, dataprovider.Close())
err := dataprovider.Initialize(providerConf, configDir)
err := dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
if _, err = os.Stat(credentialsFile); err == nil {
@ -1210,7 +1204,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoError(t, os.Remove(credentialsFile))
}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())
@ -1224,7 +1218,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
err = client.Connect()
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1232,7 +1226,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoError(t, dataprovider.Close())
assert.NoError(t, config.LoadConfig(configDir, ""))
providerConf = config.GetProviderConf()
assert.NoError(t, dataprovider.Initialize(providerConf, configDir))
assert.NoError(t, dataprovider.Initialize(providerConf, configDir, true))
}
func TestLoginInvalidFs(t *testing.T) {
@ -1240,7 +1234,7 @@ func TestLoginInvalidFs(t *testing.T) {
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "test"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
providerConf := config.GetProviderConf()
@ -1256,7 +1250,7 @@ func TestLoginInvalidFs(t *testing.T) {
client := getWebDavClient(user)
assert.Error(t, checkBasicFunc(client))
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1265,13 +1259,13 @@ func TestLoginInvalidFs(t *testing.T) {
func TestBytesRangeRequests(t *testing.T) {
u := getTestUser()
u.Username = u.Username + "1"
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
sftpUser := getTestSFTPUser()
sftpUser.FsConfig.SFTPConfig.Username = localUser.Username
for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} {
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
testFileName := "test_file.txt"
testFilePath := filepath.Join(homeBasePath, testFileName)
@ -1309,12 +1303,12 @@ func TestBytesRangeRequests(t *testing.T) {
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
@ -1324,7 +1318,7 @@ func TestGETAsPROPFIND(t *testing.T) {
u := getTestUser()
subDir1 := "/sub1"
u.Permissions[subDir1] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
rootPath := fmt.Sprintf("http://%v/%v", webDavServerAddr, user.Username)
httpClient := httpclient.GetHTTPClient()
@ -1371,13 +1365,13 @@ func TestGETAsPROPFIND(t *testing.T) {
assert.Len(t, files, 0)
// if we grant the permissions the files are listed
user.Permissions[subDir1] = []string{dataprovider.PermDownload, dataprovider.PermListItems}
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
files, err = client.ReadDir(subDir1)
assert.NoError(t, err)
assert.Len(t, files, 1)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1386,7 +1380,7 @@ func TestGETAsPROPFIND(t *testing.T) {
func TestStat(t *testing.T) {
u := getTestUser()
u.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermDownload}
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
subDir := "subdir"
@ -1401,7 +1395,7 @@ func TestStat(t *testing.T) {
err = uploadFile(testFilePath, path.Join("/", subDir, testFileName), testFileSize, client)
assert.NoError(t, err)
user.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermDownload}
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, err = client.Stat(testFileName)
assert.NoError(t, err)
@ -1410,7 +1404,7 @@ func TestStat(t *testing.T) {
err = os.Remove(testFilePath)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1430,7 +1424,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
})
err := os.MkdirAll(mappedPath, os.ModePerm)
assert.NoError(t, err)
user, _, err := httpd.AddUser(u, http.StatusOK)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
files, err := client.ReadDir(".")
@ -1454,7 +1448,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
assert.NoError(t, err)
err = uploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client)
assert.NoError(t, err)
folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
assert.NoError(t, err)
if assert.Len(t, folder, 1) {
f := folder[0]
@ -1463,7 +1457,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
}
err = uploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client)
assert.NoError(t, err)
folder, _, err = httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
folder, _, err = httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
assert.NoError(t, err)
if assert.Len(t, folder, 1) {
f := folder[0]
@ -1472,9 +1466,9 @@ func TestUploadOverwriteVfolder(t *testing.T) {
}
err = os.Remove(testFilePath)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
@ -1485,11 +1479,11 @@ func TestUploadOverwriteVfolder(t *testing.T) {
func TestMiscCommands(t *testing.T) {
u := getTestUser()
u.QuotaFiles = 100
localUser, _, err := httpd.AddUser(u, http.StatusOK)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.QuotaFiles = 100
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
dir := "testDir"
@ -1508,7 +1502,7 @@ func TestMiscCommands(t *testing.T) {
assert.NoError(t, err)
err = client.Copy(dir, dir+"_copy", false)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 6, user.UsedQuotaFiles)
assert.Equal(t, 6*testFileSize, user.UsedQuotaSize)
@ -1518,7 +1512,7 @@ func TestMiscCommands(t *testing.T) {
assert.Error(t, err)
err = client.Copy(dir+"_copy", dir+"_copy1", true)
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 9, user.UsedQuotaFiles)
assert.Equal(t, 9*testFileSize, user.UsedQuotaSize)
@ -1532,7 +1526,7 @@ func TestMiscCommands(t *testing.T) {
assert.NoError(t, err)
err = client.RemoveAll(dir + "_copy1")
assert.NoError(t, err)
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 6, user.UsedQuotaFiles)
assert.Equal(t, 6*testFileSize, user.UsedQuotaSize)
@ -1543,13 +1537,13 @@ func TestMiscCommands(t *testing.T) {
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
user.QuotaFiles = 0
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
}
}
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpd.RemoveUser(localUser, http.StatusOK)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)

View file

@ -44,7 +44,6 @@ Source: "{#MyAppDir}\sftpgo.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppDir}\sftpgo.db"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall
Source: "{#MyAppDir}\LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppDir}\sftpgo.json"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall
Source: "{#MyAppDir}\sftpgo_api_cli.exe"; DestDir: "{app}\examples\rest-api-cli"; Flags: ignoreversion; MinVersion: 10
Source: "{#MyAppDir}\templates\*"; DestDir: "{commonappdata}\{#MyAppName}\templates"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppDir}\static\*"; DestDir: "{commonappdata}\{#MyAppName}\static"; Flags: ignoreversion recursesubdirs createallsubdirs
@ -56,7 +55,6 @@ Name: "{commonappdata}\{#MyAppName}\credentials"; Permissions: everyone-full
[Icons]
Name: "{group}\Web Admin"; Filename: "http://127.0.0.1:8080/web";
Name: "{group}\Service Control"; WorkingDir: "{app}"; Filename: "powershell.exe"; Parameters: "-Command ""Start-Process cmd \""/k cd {app} & {#MyAppExeName} service --help\"" -Verb RunAs"; Comment: "Manage SFTPGo Service"
Name: "{group}\REST API CLI"; WorkingDir: "{app}\examples\rest-api-cli"; Filename: "{cmd}"; Parameters: "/k sftpgo_api_cli.exe --help"; Comment: "Manage users, folders and connections"; MinVersion: 10
Name: "{group}\Documentation"; Filename: "{#DocURL}";
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"