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 if: startsWith(matrix.os, 'windows-') != true
run: | run: |
mkdir -p output/{bash_completion,zsh_completion} mkdir -p output/{bash_completion,zsh_completion}
mkdir -p output/examples/rest-api-cli
cp sftpgo output/ cp sftpgo output/
cp sftpgo.json output/ cp sftpgo.json output/
cp -r templates output/ cp -r templates output/
cp -r static output/ cp -r static output/
cp -r init 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 bash > output/bash_completion/sftpgo
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1 ./sftpgo gen man -d output/man/man1

View file

@ -94,12 +94,6 @@ jobs:
with: with:
go-version: ${{ env.GO_VERSION }} 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 - name: Build for Linux/macOS
if: startsWith(matrix.os, 'windows-') != true 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 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: env:
MATRIX_OS: ${{ matrix.os }} 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 - name: Gather cross build info
id: cross_info id: cross_info
if: ${{ matrix.os == 'ubuntu-latest' }} if: ${{ matrix.os == 'ubuntu-latest' }}
@ -170,7 +155,7 @@ jobs:
- name: Prepare Release for Linux/macOS - name: Prepare Release for Linux/macOS
if: startsWith(matrix.os, 'windows-') != true if: startsWith(matrix.os, 'windows-') != true
run: | 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 "For documentation please take a look here:" > output/README.txt
echo "" >> output/README.txt echo "" >> output/README.txt
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> 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 completion zsh > output/zsh_completion/_sftpgo
./sftpgo gen man -d output/man/man1 ./sftpgo gen man -d output/man/man1
gzip output/man/man1/* gzip output/man/man1/*
cp examples/rest-api-cli/sftpgo_api_cli output/examples/rest-api-cli/
if [ $OS == 'linux' ] if [ $OS == 'linux' ]
then then
cp -r output output_arm64 cp -r output output_arm64
@ -254,7 +238,6 @@ jobs:
copy .\sftpgo.exe .\output copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output copy .\sftpgo.json .\output
copy .\sftpgo.db .\output copy .\sftpgo.db .\output
copy .\dist\sftpgo_api_cli.exe .\output
copy .\LICENSE .\output\LICENSE.txt copy .\LICENSE .\output\LICENSE.txt
mkdir output\templates mkdir output\templates
xcopy .\templates .\output\templates\ /E xcopy .\templates .\output\templates\ /E
@ -268,11 +251,10 @@ jobs:
- name: Prepare Portable Release for Windows - name: Prepare Portable Release for Windows
if: startsWith(matrix.os, 'windows-') if: startsWith(matrix.os, 'windows-')
run: | run: |
mkdir win-portable\examples\rest-api-cli mkdir win-portable
copy .\sftpgo.exe .\win-portable copy .\sftpgo.exe .\win-portable
copy .\sftpgo.json .\win-portable copy .\sftpgo.json .\win-portable
copy .\sftpgo.db .\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 copy .\LICENSE .\win-portable\LICENSE.txt
mkdir win-portable\templates mkdir win-portable\templates
xcopy .\templates .\win-portable\templates\ /E 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. - 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. - [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. - [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. - [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. - [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). - 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 [web based administration interface](./docs/web-admin.md)
- the [REST API](./docs/rest-api.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. 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 dataProviderConf.PreferDatabaseCredentials = true
} }
config.SetProviderConf(dataProviderConf) config.SetProviderConf(dataProviderConf)
err = dataprovider.Initialize(dataProviderConf, configDir) err = dataprovider.Initialize(dataProviderConf, configDir, false)
if err != nil { if err != nil {
logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err) logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err)
os.Exit(1) os.Exit(1)

View file

@ -179,7 +179,7 @@ func initializeDataprovider(trackQuota int) (string, error) {
if trackQuota >= 0 && trackQuota <= 2 { if trackQuota >= 0 && trackQuota <= 2 {
cfg.Config.TrackQuota = trackQuota 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 { func closeDataprovider() error {

View file

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

View file

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

View file

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

View file

@ -175,7 +175,6 @@ func Init() {
Password: "", Password: "",
ConnectionString: "", ConnectionString: "",
SQLTablesPrefix: "", SQLTablesPrefix: "",
ManageUsers: 1,
SSLMode: 0, SSLMode: 0,
TrackQuota: 1, TrackQuota: 1,
PoolSize: 0, PoolSize: 0,
@ -208,7 +207,6 @@ func Init() {
TemplatesPath: "templates", TemplatesPath: "templates",
StaticFilesPath: "static", StaticFilesPath: "static",
BackupsPath: "backups", BackupsPath: "backups",
AuthUserFile: "",
CertificateFile: "", CertificateFile: "",
CertificateKeyFile: "", CertificateKeyFile: "",
}, },
@ -749,7 +747,6 @@ func setViperDefaults() {
viper.SetDefault("data_provider.sslmode", globalConf.ProviderConf.SSLMode) viper.SetDefault("data_provider.sslmode", globalConf.ProviderConf.SSLMode)
viper.SetDefault("data_provider.connection_string", globalConf.ProviderConf.ConnectionString) viper.SetDefault("data_provider.connection_string", globalConf.ProviderConf.ConnectionString)
viper.SetDefault("data_provider.sql_tables_prefix", globalConf.ProviderConf.SQLTablesPrefix) 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.track_quota", globalConf.ProviderConf.TrackQuota)
viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize) viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize)
viper.SetDefault("data_provider.users_base_dir", globalConf.ProviderConf.UsersBaseDir) 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.templates_path", globalConf.HTTPDConfig.TemplatesPath)
viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath) viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath) 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_file", globalConf.HTTPDConfig.CertificateFile)
viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile) viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile)
viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout) 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 package dataprovider
import ( import (
"encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -23,11 +22,12 @@ const (
) )
var ( var (
usersBucket = []byte("users") usersBucket = []byte("users")
usersIDIdxBucket = []byte("users_id_idx") //usersIDIdxBucket = []byte("users_id_idx")
foldersBucket = []byte("folders") foldersBucket = []byte("folders")
dbVersionBucket = []byte("db_version") adminsBucket = []byte("admins")
dbVersionKey = []byte("version") dbVersionBucket = []byte("db_version")
dbVersionKey = []byte("version")
) )
// BoltProvider auth provider for bolt key/value store // 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) providerLog(logger.LevelWarn, "error creating users bucket: %v", err)
return err return err
} }
err = dbHandle.Update(func(tx *bolt.Tx) error {
_, e := tx.CreateBucketIfNotExists(usersIDIdxBucket)
return e
})
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err) providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err)
return err return err
@ -76,7 +72,15 @@ func initializeBoltProvider(basePath string) error {
return e return e
}) })
if err != nil { 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 return err
} }
err = dbHandle.Update(func(tx *bolt.Tx) error { 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) providerLog(logger.LevelWarn, "error creating database version bucket: %v", err)
return err return err
} }
provider = BoltProvider{dbHandle: dbHandle} provider = &BoltProvider{dbHandle: dbHandle}
} else { } else {
providerLog(logger.LevelWarn, "error creating bolt key/value store handler: %v", err) providerLog(logger.LevelWarn, "error creating bolt key/value store handler: %v", err)
} }
return err return err
} }
func (p BoltProvider) checkAvailability() error { func (p *BoltProvider) checkAvailability() error {
_, err := getBoltDatabaseVersion(p.dbHandle) _, err := getBoltDatabaseVersion(p.dbHandle)
return err 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 var user User
if password == "" { if password == "" {
return user, errors.New("Credentials cannot be null or empty") 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) 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 var user User
if len(pubKey) == 0 { if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty") 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) return checkUserAndPubKey(user, pubKey)
} }
func (p BoltProvider) getUserByID(ID int64) (User, error) { func (p *BoltProvider) updateLastLogin(username string) 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 {
return p.dbHandle.Update(func(tx *bolt.Tx) error { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err 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 { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err 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) user, err := p.userExists(username)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "unable to get quota for user %v error: %v", username, err) 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 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 var user User
err := p.dbHandle.View(func(tx *bolt.Tx) error { err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -246,13 +396,13 @@ func (p BoltProvider) userExists(username string) (User, error) {
return user, err return user, err
} }
func (p BoltProvider) addUser(user *User) error { func (p *BoltProvider) addUser(user *User) error {
err := validateUser(user) err := validateUser(user)
if err != nil { if err != nil {
return err return err
} }
return p.dbHandle.Update(func(tx *bolt.Tx) error { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, idxBucket, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -282,22 +432,17 @@ func (p BoltProvider) addUser(user *User) error {
if err != nil { if err != nil {
return err return err
} }
err = bucket.Put([]byte(user.Username), buf) return bucket.Put([]byte(user.Username), buf)
if err != nil {
return err
}
userIDAsBytes := itob(user.ID)
return idxBucket.Put(userIDAsBytes, []byte(user.Username))
}) })
} }
func (p BoltProvider) updateUser(user *User) error { func (p *BoltProvider) updateUser(user *User) error {
err := validateUser(user) err := validateUser(user)
if err != nil { if err != nil {
return err return err
} }
return p.dbHandle.Update(func(tx *bolt.Tx) error { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err 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 { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, idxBucket, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -357,23 +502,18 @@ func (p BoltProvider) deleteUser(user *User) error {
} }
} }
} }
userIDAsBytes := itob(user.ID) exists := bucket.Get([]byte(user.Username))
userName := idxBucket.Get(userIDAsBytes) if exists == nil {
if userName == nil { return &RecordNotFoundError{err: fmt.Sprintf("user %#v does not exist", user.Username)}
return &RecordNotFoundError{err: fmt.Sprintf("user with id %v does not exist", user.ID)}
} }
err = bucket.Delete(userName) return bucket.Delete([]byte(user.Username))
if err != nil {
return err
}
return idxBucket.Delete(userIDAsBytes)
}) })
} }
func (p BoltProvider) dumpUsers() ([]User, error) { func (p *BoltProvider) dumpUsers() ([]User, error) {
users := make([]User, 0, 100) users := make([]User, 0, 100)
err := p.dbHandle.View(func(tx *bolt.Tx) error { err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -398,35 +538,14 @@ func (p BoltProvider) dumpUsers() ([]User, error) {
return users, err return users, err
} }
func (p BoltProvider) getUserWithUsername(username string) ([]User, error) { func (p *BoltProvider) getUsers(limit int, offset int, order 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) {
users := make([]User, 0, limit) users := make([]User, 0, limit)
var err error var err error
if limit <= 0 { if limit <= 0 {
return users, err 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 { err = p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -472,7 +591,7 @@ func (p BoltProvider) getUsers(limit int, offset int, order string, username str
return users, err return users, err
} }
func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { func (p *BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
folders := make([]vfs.BaseVirtualFolder, 0, 50) folders := make([]vfs.BaseVirtualFolder, 0, 50)
err := p.dbHandle.View(func(tx *bolt.Tx) error { err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx) bucket, err := getFolderBucket(tx)
@ -493,7 +612,7 @@ func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return folders, err 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) folders := make([]vfs.BaseVirtualFolder, 0, limit)
var err error var err error
if limit <= 0 { if limit <= 0 {
@ -554,7 +673,7 @@ func (p BoltProvider) getFolders(limit, offset int, order, folderPath string) ([
return folders, err 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 var folder vfs.BaseVirtualFolder
err := p.dbHandle.View(func(tx *bolt.Tx) error { err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx) bucket, err := getFolderBucket(tx)
@ -567,7 +686,7 @@ func (p BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error
return folder, err return folder, err
} }
func (p BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error { func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
err := validateFolder(folder) err := validateFolder(folder)
if err != nil { if err != nil {
return err 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 { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx) bucket, err := getFolderBucket(tx)
if err != nil { if err != nil {
return err return err
} }
usersBucket, _, err := getBuckets(tx) usersBucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err 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 { return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := getFolderBucket(tx) bucket, err := getFolderBucket(tx)
if err != nil { 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) folder, err := p.getFolderByPath(mappedPath)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "unable to get quota for folder %#v error: %v", mappedPath, err) 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 return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
} }
func (p BoltProvider) close() error { func (p *BoltProvider) close() error {
return p.dbHandle.Close() return p.dbHandle.Close()
} }
func (p BoltProvider) reloadConfig() error { func (p *BoltProvider) reloadConfig() error {
return nil return nil
} }
// initializeDatabase does nothing, no initilization is needed for bolt provider // initializeDatabase does nothing, no initilization is needed for bolt provider
func (p BoltProvider) initializeDatabase() error { func (p *BoltProvider) initializeDatabase() error {
return ErrNoInitRequired return ErrNoInitRequired
} }
func (p BoltProvider) migrateDatabase() error { func (p *BoltProvider) migrateDatabase() error {
dbVersion, err := getBoltDatabaseVersion(p.dbHandle) dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
if err != nil { if err != nil {
return err 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) dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
if err != nil { if err != nil {
return err return err
@ -762,16 +881,12 @@ func updateBoltDatabaseFromV4(dbHandle *bolt.DB) error {
return updateDatabaseFrom4To5(dbHandle) 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) { func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) {
var user User var user User
err := json.Unmarshal(u, &user) err := json.Unmarshal(u, &user)
if err != nil {
return user, err
}
if len(user.VirtualFolders) > 0 { if len(user.VirtualFolders) > 0 {
var folders []vfs.VirtualFolder var folders []vfs.VirtualFolder
for _, folder := range user.VirtualFolders { 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 { func updateV4BoltCompatUser(dbHandle *bolt.DB, user compatUserV4) error {
return dbHandle.Update(func(tx *bolt.Tx) error { return dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -893,7 +1008,7 @@ func updateV4BoltUser(dbHandle *bolt.DB, user User) error {
return err return err
} }
return dbHandle.Update(func(tx *bolt.Tx) error { return dbHandle.Update(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err 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 var err error
bucket := tx.Bucket(usersBucket) bucket := tx.Bucket(usersBucket)
idxBucket := tx.Bucket(usersIDIdxBucket) if bucket == nil {
if bucket == nil || idxBucket == nil { err = errors.New("unable to find required buckets, bolt database structure not correcly defined")
err = fmt.Errorf("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) { 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") providerLog(logger.LevelInfo, "updating bolt database version: 2 -> 3")
users := []User{} users := []User{}
err := dbHandle.View(func(tx *bolt.Tx) error { err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -1011,7 +1135,7 @@ func updateDatabaseFrom3To4(dbHandle *bolt.DB) error {
foldersToScan := []string{} foldersToScan := []string{}
users := []userCompactVFolders{} users := []userCompactVFolders{}
err := dbHandle.View(func(tx *bolt.Tx) error { err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -1075,7 +1199,7 @@ func downgradeBoltDatabaseFrom5To4(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "downgrading bolt database version: 5 -> 4") providerLog(logger.LevelInfo, "downgrading bolt database version: 5 -> 4")
users := []compatUserV4{} users := []compatUserV4{}
err := dbHandle.View(func(tx *bolt.Tx) error { err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -1116,7 +1240,7 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "updating bolt database version: 4 -> 5") providerLog(logger.LevelInfo, "updating bolt database version: 4 -> 5")
users := []User{} users := []User{}
err := dbHandle.View(func(tx *bolt.Tx) error { err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
@ -1154,13 +1278,13 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error {
func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) { func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) {
usernames := []string{} usernames := []string{}
err := dbHandle.View(func(tx *bolt.Tx) error { err := dbHandle.View(func(tx *bolt.Tx) error {
_, idxBucket, err := getBuckets(tx) bucket, err := getUsersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
cursor := idxBucket.Cursor() cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() { for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() {
usernames = append(usernames, string(v)) usernames = append(usernames, string(k))
} }
return nil return nil
}) })

View file

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

View file

@ -26,14 +26,16 @@ type memoryProviderHandle struct {
isClosed bool isClosed bool
// slice with ordered usernames // slice with ordered usernames
usernames []string usernames []string
// mapping between ID and username
usersIdx map[int64]string
// map for users, username is the key // map for users, username is the key
users map[string]User users map[string]User
// map for virtual folders, MappedPath is the key // map for virtual folders, MappedPath is the key
vfolders map[string]vfs.BaseVirtualFolder vfolders map[string]vfs.BaseVirtualFolder
// slice with ordered folders mapped path // slice with ordered folders mapped path
vfoldersPaths []string 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 // MemoryProvider auth provider for a memory store
@ -50,15 +52,16 @@ func initializeMemoryProvider(basePath string) {
configFile = filepath.Join(basePath, configFile) configFile = filepath.Join(basePath, configFile)
} }
} }
provider = MemoryProvider{ provider = &MemoryProvider{
dbHandle: &memoryProviderHandle{ dbHandle: &memoryProviderHandle{
isClosed: false, isClosed: false,
usernames: []string{}, usernames: []string{},
usersIdx: make(map[int64]string), users: make(map[string]User),
users: make(map[string]User), vfolders: make(map[string]vfs.BaseVirtualFolder),
vfolders: make(map[string]vfs.BaseVirtualFolder), vfoldersPaths: []string{},
vfoldersPaths: []string{}, admins: make(map[string]Admin),
configFile: configFile, adminsUsernames: []string{},
configFile: configFile,
}, },
} }
if err := provider.reloadConfig(); err != nil { 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() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -76,7 +79,7 @@ func (p MemoryProvider) checkAvailability() error {
return nil return nil
} }
func (p MemoryProvider) close() error { func (p *MemoryProvider) close() error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -86,7 +89,7 @@ func (p MemoryProvider) close() error {
return nil 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 var user User
if password == "" { if password == "" {
return user, errors.New("Credentials cannot be null or empty") 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) 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 var user User
if len(pubKey) == 0 { if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty") 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) return checkUserAndPubKey(user, pubKey)
} }
func (p MemoryProvider) getUserByID(ID int64) (User, error) { func (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
p.dbHandle.Lock() admin, err := p.adminExists(username)
defer p.dbHandle.Unlock() if err != nil {
if p.dbHandle.isClosed { providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
return User{}, errMemoryProviderClosed return admin, err
} }
if val, ok := p.dbHandle.usersIdx[ID]; ok { err = admin.checkUserAndPass(password, ip)
return p.userExistsInternal(val) return admin, err
}
return User{}, &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)}
} }
func (p MemoryProvider) updateLastLogin(username string) error { func (p *MemoryProvider) updateLastLogin(username string) error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -139,7 +140,7 @@ func (p MemoryProvider) updateLastLogin(username string) error {
return nil 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() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -164,7 +165,7 @@ func (p MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64
return nil return nil
} }
func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) { func (p *MemoryProvider) getUsedQuota(username string) (int, int64, error) {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -178,7 +179,7 @@ func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) {
return user.UsedQuotaFiles, user.UsedQuotaSize, err return user.UsedQuotaFiles, user.UsedQuotaSize, err
} }
func (p MemoryProvider) addUser(user *User) error { func (p *MemoryProvider) addUser(user *User) error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -199,13 +200,12 @@ func (p MemoryProvider) addUser(user *User) error {
user.LastLogin = 0 user.LastLogin = 0
user.VirtualFolders = p.joinVirtualFoldersFields(user) user.VirtualFolders = p.joinVirtualFoldersFields(user)
p.dbHandle.users[user.Username] = user.getACopy() p.dbHandle.users[user.Username] = user.getACopy()
p.dbHandle.usersIdx[user.ID] = user.Username
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username) p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
sort.Strings(p.dbHandle.usernames) sort.Strings(p.dbHandle.usernames)
return nil return nil
} }
func (p MemoryProvider) updateUser(user *User) error { func (p *MemoryProvider) updateUser(user *User) error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -227,12 +227,13 @@ func (p MemoryProvider) updateUser(user *User) error {
user.UsedQuotaSize = u.UsedQuotaSize user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles user.UsedQuotaFiles = u.UsedQuotaFiles
user.LastLogin = u.LastLogin user.LastLogin = u.LastLogin
user.ID = u.ID
// pre-login and external auth hook will use the passed *user so save a copy // pre-login and external auth hook will use the passed *user so save a copy
p.dbHandle.users[user.Username] = user.getACopy() p.dbHandle.users[user.Username] = user.getACopy()
return nil return nil
} }
func (p MemoryProvider) deleteUser(user *User) error { func (p *MemoryProvider) deleteUser(user *User) error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -246,7 +247,6 @@ func (p MemoryProvider) deleteUser(user *User) error {
p.removeUserFromFolderMapping(oldFolder.MappedPath, u.Username) p.removeUserFromFolderMapping(oldFolder.MappedPath, u.Username)
} }
delete(p.dbHandle.users, user.Username) delete(p.dbHandle.users, user.Username)
delete(p.dbHandle.usersIdx, user.ID)
// this could be more efficient // this could be more efficient
p.dbHandle.usernames = make([]string, 0, len(p.dbHandle.users)) p.dbHandle.usernames = make([]string, 0, len(p.dbHandle.users))
for username := range p.dbHandle.users { for username := range p.dbHandle.users {
@ -256,7 +256,7 @@ func (p MemoryProvider) deleteUser(user *User) error {
return nil return nil
} }
func (p MemoryProvider) dumpUsers() ([]User, error) { func (p *MemoryProvider) dumpUsers() ([]User, error) {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
users := make([]User, 0, len(p.dbHandle.usernames)) users := make([]User, 0, len(p.dbHandle.usernames))
@ -276,7 +276,7 @@ func (p MemoryProvider) dumpUsers() ([]User, error) {
return users, err return users, err
} }
func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) { func (p *MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
folders := make([]vfs.BaseVirtualFolder, 0, len(p.dbHandle.vfoldersPaths)) folders := make([]vfs.BaseVirtualFolder, 0, len(p.dbHandle.vfoldersPaths))
@ -289,7 +289,7 @@ func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
return folders, nil 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) users := make([]User, 0, limit)
var err error var err error
p.dbHandle.Lock() p.dbHandle.Lock()
@ -300,16 +300,6 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s
if limit <= 0 { if limit <= 0 {
return users, err 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 itNum := 0
if order == OrderASC { if order == OrderASC {
for _, username := range p.dbHandle.usernames { 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 return users, err
} }
func (p MemoryProvider) userExists(username string) (User, error) { func (p *MemoryProvider) userExists(username string) (User, error) {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -353,14 +343,152 @@ func (p MemoryProvider) userExists(username string) (User, error) {
return p.userExistsInternal(username) 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 { if val, ok := p.dbHandle.users[username]; ok {
return val.getACopy(), nil return val.getACopy(), nil
} }
return User{}, &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)} 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() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -383,7 +511,7 @@ func (p MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeA
return nil return nil
} }
func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) { func (p *MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -397,7 +525,7 @@ func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err 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 var folders []vfs.VirtualFolder
for _, folder := range user.VirtualFolders { for _, folder := range user.VirtualFolders {
f, err := p.addOrGetFolderInternal(folder.MappedPath, user.Username, folder.UsedQuotaSize, folder.UsedQuotaFiles, 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 return folders
} }
func (p MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) { func (p *MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) {
folder, err := p.folderExistsInternal(mappedPath) folder, err := p.folderExistsInternal(mappedPath)
if err == nil { if err == nil {
var usernames []string 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 p.dbHandle.vfolders[folder.MappedPath] = folder
if !utils.IsStringInSlice(folder.MappedPath, p.dbHandle.vfoldersPaths) { if !utils.IsStringInSlice(folder.MappedPath, p.dbHandle.vfoldersPaths) {
p.dbHandle.vfoldersPaths = append(p.dbHandle.vfoldersPaths, folder.MappedPath) 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) folder, err := p.folderExistsInternal(mappedPath)
if _, ok := err.(*RecordNotFoundError); ok { if _, ok := err.(*RecordNotFoundError); ok {
folder := vfs.BaseVirtualFolder{ folder := vfs.BaseVirtualFolder{
@ -456,14 +584,14 @@ func (p MemoryProvider) addOrGetFolderInternal(mappedPath, username string, used
return folder, err 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 { if val, ok := p.dbHandle.vfolders[mappedPath]; ok {
return val, nil return val, nil
} }
return vfs.BaseVirtualFolder{}, &RecordNotFoundError{err: fmt.Sprintf("folder %#v does not exist", mappedPath)} 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) folders := make([]vfs.BaseVirtualFolder, 0, limit)
var err error var err error
p.dbHandle.Lock() p.dbHandle.Lock()
@ -514,7 +642,7 @@ func (p MemoryProvider) getFolders(limit, offset int, order, folderPath string)
return folders, err return folders, err
} }
func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) { func (p *MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -523,7 +651,7 @@ func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolde
return p.folderExistsInternal(mappedPath) return p.folderExistsInternal(mappedPath)
} }
func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error { func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -544,7 +672,7 @@ func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return nil return nil
} }
func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error { func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
@ -577,17 +705,17 @@ func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
return nil return nil
} }
func (p MemoryProvider) getNextID() int64 { func (p *MemoryProvider) getNextID() int64 {
nextID := int64(1) nextID := int64(1)
for id := range p.dbHandle.usersIdx { for _, v := range p.dbHandle.users {
if id >= nextID { if v.ID >= nextID {
nextID = id + 1 nextID = v.ID + 1
} }
} }
return nextID return nextID
} }
func (p MemoryProvider) getNextFolderID() int64 { func (p *MemoryProvider) getNextFolderID() int64 {
nextID := int64(1) nextID := int64(1)
for _, v := range p.dbHandle.vfolders { for _, v := range p.dbHandle.vfolders {
if v.ID >= nextID { if v.ID >= nextID {
@ -597,17 +725,28 @@ func (p MemoryProvider) getNextFolderID() int64 {
return nextID 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() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
p.dbHandle.usernames = []string{} p.dbHandle.usernames = []string{}
p.dbHandle.usersIdx = make(map[int64]string)
p.dbHandle.users = make(map[string]User) p.dbHandle.users = make(map[string]User)
p.dbHandle.vfoldersPaths = []string{} p.dbHandle.vfoldersPaths = []string{}
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder) 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 == "" { if p.dbHandle.configFile == "" {
providerLog(logger.LevelDebug, "no users configuration file defined") providerLog(logger.LevelDebug, "no users configuration file defined")
return nil return nil
@ -676,14 +815,14 @@ func (p MemoryProvider) reloadConfig() error {
} }
// initializeDatabase does nothing, no initilization is needed for memory provider // initializeDatabase does nothing, no initilization is needed for memory provider
func (p MemoryProvider) initializeDatabase() error { func (p *MemoryProvider) initializeDatabase() error {
return ErrNoInitRequired return ErrNoInitRequired
} }
func (p MemoryProvider) migrateDatabase() error { func (p *MemoryProvider) migrateDatabase() error {
return ErrNoInitRequired 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") 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;" "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;" mysqlV6SQL = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;"
mysqlV6DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `additional_info`;" 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 // MySQLProvider auth provider for MySQL/MariaDB database
@ -65,7 +69,7 @@ func initializeMySQLProvider() error {
dbHandle.SetMaxIdleConns(2) dbHandle.SetMaxIdleConns(2)
} }
dbHandle.SetConnMaxLifetime(240 * time.Second) dbHandle.SetConnMaxLifetime(240 * time.Second)
provider = MySQLProvider{dbHandle: dbHandle} provider = &MySQLProvider{dbHandle: dbHandle}
} else { } else {
providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v", providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v",
getMySQLConnectionString(true), err) getMySQLConnectionString(true), err)
@ -87,98 +91,122 @@ func getMySQLConnectionString(redactedPwd bool) string {
return connectionString return connectionString
} }
func (p MySQLProvider) checkAvailability() error { func (p *MySQLProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle) 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) 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) return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
} }
func (p MySQLProvider) getUserByID(ID int64) (User, error) { func (p *MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonGetUserByID(ID, p.dbHandle)
}
func (p MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle) 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) return sqlCommonGetUsedQuota(username, p.dbHandle)
} }
func (p MySQLProvider) updateLastLogin(username string) error { func (p *MySQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle) return sqlCommonUpdateLastLogin(username, p.dbHandle)
} }
func (p MySQLProvider) userExists(username string) (User, error) { func (p *MySQLProvider) userExists(username string) (User, error) {
return sqlCommonCheckUserExists(username, p.dbHandle) return sqlCommonGetUserByUsername(username, p.dbHandle)
} }
func (p MySQLProvider) addUser(user *User) error { func (p *MySQLProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle) return sqlCommonAddUser(user, p.dbHandle)
} }
func (p MySQLProvider) updateUser(user *User) error { func (p *MySQLProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle) return sqlCommonUpdateUser(user, p.dbHandle)
} }
func (p MySQLProvider) deleteUser(user *User) error { func (p *MySQLProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle) return sqlCommonDeleteUser(user, p.dbHandle)
} }
func (p MySQLProvider) dumpUsers() ([]User, error) { func (p *MySQLProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle) return sqlCommonDumpUsers(p.dbHandle)
} }
func (p MySQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { func (p *MySQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle) 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) 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) 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) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle) 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) 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) 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) 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) 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() return p.dbHandle.Close()
} }
func (p MySQLProvider) reloadConfig() error { func (p *MySQLProvider) reloadConfig() error {
return nil return nil
} }
// initializeDatabase creates the initial database structure // initializeDatabase creates the initial database structure
func (p MySQLProvider) initializeDatabase() error { func (p *MySQLProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 { if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired return ErrNoInitRequired
@ -206,7 +234,7 @@ func (p MySQLProvider) initializeDatabase() error {
return tx.Commit() return tx.Commit()
} }
func (p MySQLProvider) migrateDatabase() error { func (p *MySQLProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil { if err != nil {
return err return err
@ -226,6 +254,8 @@ func (p MySQLProvider) migrateDatabase() error {
return updateMySQLDatabaseFromV4(p.dbHandle) return updateMySQLDatabaseFromV4(p.dbHandle)
case 5: case 5:
return updateMySQLDatabaseFromV5(p.dbHandle) return updateMySQLDatabaseFromV5(p.dbHandle)
case 6:
return updateMySQLDatabaseFromV6(p.dbHandle)
default: default:
if dbVersion.Version > sqlDatabaseVersion { if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version, 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) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil { if err != nil {
return err return err
@ -247,6 +277,16 @@ func (p MySQLProvider) revertDatabase(targetVersion int) error {
return fmt.Errorf("current version match target version, nothing to do") return fmt.Errorf("current version match target version, nothing to do")
} }
switch dbVersion.Version { 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: case 6:
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle) err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
if err != nil { if err != nil {
@ -293,7 +333,15 @@ func updateMySQLDatabaseFromV4(dbHandle *sql.DB) error {
} }
func updateMySQLDatabaseFromV5(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 { func updateMySQLDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -325,6 +373,20 @@ func updateMySQLDatabaseFrom5To6(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) 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 { func downgradeMySQLDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5") logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "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;` pgsqlV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
pgsqlV6DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "additional_info" CASCADE;` 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 // PGSQLProvider auth provider for PostgreSQL database
@ -65,7 +70,7 @@ func initializePGSQLProvider() error {
dbHandle.SetMaxIdleConns(2) dbHandle.SetMaxIdleConns(2)
} }
dbHandle.SetConnMaxLifetime(240 * time.Second) dbHandle.SetConnMaxLifetime(240 * time.Second)
provider = PGSQLProvider{dbHandle: dbHandle} provider = &PGSQLProvider{dbHandle: dbHandle}
} else { } else {
providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v", providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v",
getPGSQLConnectionString(true), err) getPGSQLConnectionString(true), err)
@ -88,98 +93,122 @@ func getPGSQLConnectionString(redactedPwd bool) string {
return connectionString return connectionString
} }
func (p PGSQLProvider) checkAvailability() error { func (p *PGSQLProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle) 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) 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) return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
} }
func (p PGSQLProvider) getUserByID(ID int64) (User, error) { func (p *PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonGetUserByID(ID, p.dbHandle)
}
func (p PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle) 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) return sqlCommonGetUsedQuota(username, p.dbHandle)
} }
func (p PGSQLProvider) updateLastLogin(username string) error { func (p *PGSQLProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle) return sqlCommonUpdateLastLogin(username, p.dbHandle)
} }
func (p PGSQLProvider) userExists(username string) (User, error) { func (p *PGSQLProvider) userExists(username string) (User, error) {
return sqlCommonCheckUserExists(username, p.dbHandle) return sqlCommonGetUserByUsername(username, p.dbHandle)
} }
func (p PGSQLProvider) addUser(user *User) error { func (p *PGSQLProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle) return sqlCommonAddUser(user, p.dbHandle)
} }
func (p PGSQLProvider) updateUser(user *User) error { func (p *PGSQLProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle) return sqlCommonUpdateUser(user, p.dbHandle)
} }
func (p PGSQLProvider) deleteUser(user *User) error { func (p *PGSQLProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle) return sqlCommonDeleteUser(user, p.dbHandle)
} }
func (p PGSQLProvider) dumpUsers() ([]User, error) { func (p *PGSQLProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle) return sqlCommonDumpUsers(p.dbHandle)
} }
func (p PGSQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { func (p *PGSQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle) 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) 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) 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) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle) 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) 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) 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) 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) 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() return p.dbHandle.Close()
} }
func (p PGSQLProvider) reloadConfig() error { func (p *PGSQLProvider) reloadConfig() error {
return nil return nil
} }
// initializeDatabase creates the initial database structure // initializeDatabase creates the initial database structure
func (p PGSQLProvider) initializeDatabase() error { func (p *PGSQLProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 { if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired return ErrNoInitRequired
@ -207,7 +236,7 @@ func (p PGSQLProvider) initializeDatabase() error {
return tx.Commit() return tx.Commit()
} }
func (p PGSQLProvider) migrateDatabase() error { func (p *PGSQLProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil { if err != nil {
return err return err
@ -227,6 +256,8 @@ func (p PGSQLProvider) migrateDatabase() error {
return updatePGSQLDatabaseFromV4(p.dbHandle) return updatePGSQLDatabaseFromV4(p.dbHandle)
case 5: case 5:
return updatePGSQLDatabaseFromV5(p.dbHandle) return updatePGSQLDatabaseFromV5(p.dbHandle)
case 6:
return updatePGSQLDatabaseFromV6(p.dbHandle)
default: default:
if dbVersion.Version > sqlDatabaseVersion { if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version, 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) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil { if err != nil {
return err return err
@ -248,6 +279,16 @@ func (p PGSQLProvider) revertDatabase(targetVersion int) error {
return fmt.Errorf("current version match target version, nothing to do") return fmt.Errorf("current version match target version, nothing to do")
} }
switch dbVersion.Version { 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: case 6:
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle) err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
if err != nil { if err != nil {
@ -294,7 +335,15 @@ func updatePGSQLDatabaseFromV4(dbHandle *sql.DB) error {
} }
func updatePGSQLDatabaseFromV5(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 { func updatePGSQLDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -326,6 +375,20 @@ func updatePGSQLDatabaseFrom5To6(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) 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 { func downgradePGSQLDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5") logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5") providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")

View file

@ -14,7 +14,7 @@ import (
) )
const ( const (
sqlDatabaseVersion = 6 sqlDatabaseVersion = 7
initialDBVersionSQL = "INSERT INTO {{schema_version}} (version) VALUES (1);" initialDBVersionSQL = "INSERT INTO {{schema_version}} (version) VALUES (1);"
defaultSQLQueryTimeout = 10 * time.Second defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second longSQLQueryTimeout = 60 * time.Second
@ -26,7 +26,174 @@ type sqlQuerier interface {
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 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 var user User
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
@ -39,7 +206,7 @@ func getUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
defer stmt.Close() defer stmt.Close()
row := stmt.QueryRowContext(ctx, username) row := stmt.QueryRowContext(ctx, username)
user, err = getUserFromDbRow(row, nil) user, err = getUserFromDbRow(row)
if err != nil { if err != nil {
return user, err return user, err
} }
@ -51,7 +218,7 @@ func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHan
if password == "" { if password == "" {
return user, errors.New("Credentials cannot be null or empty") return user, errors.New("Credentials cannot be null or empty")
} }
user, err := getUserByUsername(username, dbHandle) user, err := sqlCommonGetUserByUsername(username, dbHandle)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err) providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, err return user, err
@ -64,7 +231,7 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, dbHandle *sq
if len(pubKey) == 0 { if len(pubKey) == 0 {
return user, "", errors.New("Credentials cannot be null or empty") return user, "", errors.New("Credentials cannot be null or empty")
} }
user, err := getUserByUsername(username, dbHandle) user, err := sqlCommonGetUserByUsername(username, dbHandle)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err) providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, "", err return user, "", err
@ -78,26 +245,6 @@ func sqlCommonCheckAvailability(dbHandle *sql.DB) error {
return dbHandle.PingContext(ctx) 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 { func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
@ -158,25 +305,6 @@ func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
return err 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 { func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
err := validateUser(user) err := validateUser(user)
if err != nil { if err != nil {
@ -317,7 +445,7 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
u, err := getUserFromDbRow(nil, rows) u, err := getUserFromDbRow(rows)
if err != nil { if err != nil {
return users, err return users, err
} }
@ -327,30 +455,30 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {
} }
users = append(users, u) users = append(users, u)
} }
err = rows.Err()
if err != nil {
return users, err
}
return getUsersWithVirtualFolders(users, dbHandle) 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) users := make([]User, 0, limit)
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
q := getUsersQuery(order, username) q := getUsersQuery(order)
stmt, err := dbHandle.PrepareContext(ctx, q) stmt, err := dbHandle.PrepareContext(ctx, q)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err) providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
return nil, err return nil, err
} }
defer stmt.Close() defer stmt.Close()
var rows *sql.Rows
if len(username) > 0 { rows, err := stmt.QueryContext(ctx, limit, offset)
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
}
if err == nil { if err == nil {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
u, err := getUserFromDbRow(nil, rows) u, err := getUserFromDbRow(rows)
if err != nil { if err != nil {
return users, err return users, err
} }
@ -384,7 +512,47 @@ func updateUserPermissionsFromDb(user *User, permissions string) error {
return err 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 user User
var permissions sql.NullString var permissions sql.NullString
var password 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 filters sql.NullString
var fsConfig sql.NullString var fsConfig sql.NullString
var additionalInfo 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,
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.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate, &user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig, &additionalInfo)
&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)
}
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return user, &RecordNotFoundError{err: err.Error()} 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}}"; DROP TABLE "{{users}}";
ALTER TABLE "new__users" RENAME TO "{{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 // SQLiteProvider auth provider for SQLite database
@ -109,7 +113,7 @@ func initializeSQLiteProvider(basePath string) error {
if err == nil { if err == nil {
providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %#v", connectionString) providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %#v", connectionString)
dbHandle.SetMaxOpenConns(1) dbHandle.SetMaxOpenConns(1)
provider = SQLiteProvider{dbHandle: dbHandle} provider = &SQLiteProvider{dbHandle: dbHandle}
} else { } else {
providerLog(logger.LevelWarn, "error creating sqlite database handler, connection string: %#v, error: %v", providerLog(logger.LevelWarn, "error creating sqlite database handler, connection string: %#v, error: %v",
connectionString, err) connectionString, err)
@ -117,98 +121,122 @@ func initializeSQLiteProvider(basePath string) error {
return err return err
} }
func (p SQLiteProvider) checkAvailability() error { func (p *SQLiteProvider) checkAvailability() error {
return sqlCommonCheckAvailability(p.dbHandle) 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) 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) return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
} }
func (p SQLiteProvider) getUserByID(ID int64) (User, error) { func (p *SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonGetUserByID(ID, p.dbHandle)
}
func (p SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle) 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) return sqlCommonGetUsedQuota(username, p.dbHandle)
} }
func (p SQLiteProvider) updateLastLogin(username string) error { func (p *SQLiteProvider) updateLastLogin(username string) error {
return sqlCommonUpdateLastLogin(username, p.dbHandle) return sqlCommonUpdateLastLogin(username, p.dbHandle)
} }
func (p SQLiteProvider) userExists(username string) (User, error) { func (p *SQLiteProvider) userExists(username string) (User, error) {
return sqlCommonCheckUserExists(username, p.dbHandle) return sqlCommonGetUserByUsername(username, p.dbHandle)
} }
func (p SQLiteProvider) addUser(user *User) error { func (p *SQLiteProvider) addUser(user *User) error {
return sqlCommonAddUser(user, p.dbHandle) return sqlCommonAddUser(user, p.dbHandle)
} }
func (p SQLiteProvider) updateUser(user *User) error { func (p *SQLiteProvider) updateUser(user *User) error {
return sqlCommonUpdateUser(user, p.dbHandle) return sqlCommonUpdateUser(user, p.dbHandle)
} }
func (p SQLiteProvider) deleteUser(user *User) error { func (p *SQLiteProvider) deleteUser(user *User) error {
return sqlCommonDeleteUser(user, p.dbHandle) return sqlCommonDeleteUser(user, p.dbHandle)
} }
func (p SQLiteProvider) dumpUsers() ([]User, error) { func (p *SQLiteProvider) dumpUsers() ([]User, error) {
return sqlCommonDumpUsers(p.dbHandle) return sqlCommonDumpUsers(p.dbHandle)
} }
func (p SQLiteProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) { func (p *SQLiteProvider) getUsers(limit int, offset int, order string) ([]User, error) {
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle) 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) 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) 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) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle) 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) 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) 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) 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) 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() return p.dbHandle.Close()
} }
func (p SQLiteProvider) reloadConfig() error { func (p *SQLiteProvider) reloadConfig() error {
return nil return nil
} }
// initializeDatabase creates the initial database structure // initializeDatabase creates the initial database structure
func (p SQLiteProvider) initializeDatabase() error { func (p *SQLiteProvider) initializeDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
if err == nil && dbVersion.Version > 0 { if err == nil && dbVersion.Version > 0 {
return ErrNoInitRequired return ErrNoInitRequired
@ -236,7 +264,7 @@ func (p SQLiteProvider) initializeDatabase() error {
return tx.Commit() return tx.Commit()
} }
func (p SQLiteProvider) migrateDatabase() error { func (p *SQLiteProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil { if err != nil {
return err return err
@ -256,6 +284,8 @@ func (p SQLiteProvider) migrateDatabase() error {
return updateSQLiteDatabaseFromV4(p.dbHandle) return updateSQLiteDatabaseFromV4(p.dbHandle)
case 5: case 5:
return updateSQLiteDatabaseFromV5(p.dbHandle) return updateSQLiteDatabaseFromV5(p.dbHandle)
case 6:
return updateSQLiteDatabaseFromV6(p.dbHandle)
default: default:
if dbVersion.Version > sqlDatabaseVersion { if dbVersion.Version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version, 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) dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil { if err != nil {
return err return err
@ -277,6 +307,16 @@ func (p SQLiteProvider) revertDatabase(targetVersion int) error {
return fmt.Errorf("current version match target version, nothing to do") return fmt.Errorf("current version match target version, nothing to do")
} }
switch dbVersion.Version { 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: case 6:
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle) err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
if err != nil { if err != nil {
@ -323,7 +363,15 @@ func updateSQLiteDatabaseFromV4(dbHandle *sql.DB) error {
} }
func updateSQLiteDatabaseFromV5(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 { func updateSQLiteDatabaseFrom1To2(dbHandle *sql.DB) error {
@ -355,6 +403,20 @@ func updateSQLiteDatabaseFrom5To6(dbHandle *sql.DB) error {
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6) 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 { func downgradeSQLiteDatabaseFrom6To5(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 6 -> 5") logger.InfoToConsole("downgrading database version: 6 -> 5")
providerLog(logger.LevelInfo, "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," + 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" "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" 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 { func getSQLPlaceholders() []string {
@ -26,19 +27,40 @@ func getSQLPlaceholders() []string {
return placeholders 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 { func getUserByUsernameQuery() string {
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0]) return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
} }
func getUserByIDQuery() string { func getUsersQuery(order string) 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])
}
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectUserFields, sqlTableUsers, return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectUserFields, sqlTableUsers,
order, sqlPlaceholders[0], sqlPlaceholders[1]) order, sqlPlaceholders[0], sqlPlaceholders[1])
} }

View file

@ -20,7 +20,7 @@ import (
"github.com/drakkan/sftpgo/vfs" "github.com/drakkan/sftpgo/vfs"
) )
// Available permissions for SFTP users // Available permissions for SFTPGo users
const ( const (
// All permissions are granted // All permissions are granted
PermAny = "*" PermAny = "*"
@ -802,26 +802,12 @@ func (u *User) GetExpirationDateAsString() string {
// GetAllowedIPAsString returns the allowed IP as comma separated string // GetAllowedIPAsString returns the allowed IP as comma separated string
func (u User) GetAllowedIPAsString() string { func (u User) GetAllowedIPAsString() string {
result := "" return strings.Join(u.Filters.AllowedIP, ",")
for _, IPMask := range u.Filters.AllowedIP {
if len(result) > 0 {
result += ","
}
result += IPMask
}
return result
} }
// GetDeniedIPAsString returns the denied IP as comma separated string // GetDeniedIPAsString returns the denied IP as comma separated string
func (u User) GetDeniedIPAsString() string { func (u User) GetDeniedIPAsString() string {
result := "" return strings.Join(u.Filters.DeniedIP, ",")
for _, IPMask := range u.Filters.DeniedIP {
if len(result) > 0 {
result += ","
}
result += IPMask
}
return result
} }
// SetEmptySecretsIfNil sets the secrets to empty if nil // 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: 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 - 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. - `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. - `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. - `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. - `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` - `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` - `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 - `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: - `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 - 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 - 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 - `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 - `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 - `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_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. - `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) - **"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. 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 - username: `admin`
ProxyPass /api/v1 http://127.0.0.1:8080/api/v1 - password: `password`
ProxyPassReverse /api/v1 http://127.0.0.1:8080/api/v1
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 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.
<Location /api/v1>
AuthType Digest
AuthName "Private"
AuthDigestDomain "/api/v1"
AuthDigestProvider file
AuthUserFile "/etc/httpd/conf/auth_digest"
Require valid-user
</Location>
```
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"). 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 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 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/)

View file

@ -61,8 +61,6 @@ sudo systemctl start sftpgo
sudo systemctl status sftpgo sudo systemctl status sftpgo
# automatically start sftpgo on boot # automatically start sftpgo on boot
sudo systemctl enable sftpgo 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 # optional, create shell completion script, for example for bash
sudo sh -c '/usr/bin/sftpgo gen completion bash > /usr/share/bash-completion/completions/sftpgo' sudo sh -c '/usr/bin/sftpgo gen completion bash > /usr/share/bash-completion/completions/sftpgo'
# optional, create man pages # 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 sudo launchctl load -w /Library/LaunchDaemons/com.github.drakkan.sftpgo.plist
# verify that the service is started # verify that the service is started
sudo launchctl list com.github.drakkan.sftpgo 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 ## Windows

View file

@ -1,8 +1,13 @@
# Web Admin # 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: 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) [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 # 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. `sftpgo_api_cli` is a very simple command line client for `SFTPGo` REST API written in python.
It has the following requirements: It has the following requirements:

View file

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

View file

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

21
go.mod
View file

@ -3,34 +3,39 @@ module github.com/drakkan/sftpgo
go 1.15 go 1.15
require ( 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 cloud.google.com/go/storage v1.12.0
github.com/Azure/azure-storage-blob-go v0.12.0 github.com/Azure/azure-storage-blob-go v0.12.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b 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/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d
github.com/fclairamb/ftpserverlib v0.12.0 github.com/fclairamb/ftpserverlib v0.12.0
github.com/frankban/quicktest v1.11.2 // indirect github.com/frankban/quicktest v1.11.2 // indirect
github.com/go-chi/chi v1.5.1 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-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/go-sql-driver/mysql v1.5.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 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/grandcat/zeroconf v1.0.0
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 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/lib/pq v1.9.0
github.com/magiconair/properties v1.8.4 // indirect github.com/magiconair/properties v1.8.4 // indirect
github.com/mattn/go-sqlite3 v1.14.6 github.com/mattn/go-sqlite3 v1.14.6
github.com/miekg/dns v1.1.35 // indirect github.com/miekg/dns v1.1.35 // indirect
github.com/minio/sha256-simd v0.1.1 github.com/minio/sha256-simd v0.1.1
github.com/minio/sio v0.2.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/otiai10/copy v1.4.2
github.com/pelletier/go-toml v1.8.1 // indirect github.com/pelletier/go-toml v1.8.1 // indirect
github.com/pires/go-proxyproto v0.3.3 github.com/pires/go-proxyproto v0.3.3
github.com/pkg/sftp v1.12.1-0.20201128220914-b5b6f3393fe9 github.com/pkg/sftp v1.12.1-0.20201128220914-b5b6f3393fe9
github.com/prometheus/client_golang v1.9.0 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/cors v1.7.1-0.20200626170627-8b4a00bd362b
github.com/rs/xid v1.2.1 github.com/rs/xid v1.2.1
github.com/rs/zerolog v1.20.0 github.com/rs/zerolog v1.20.0
@ -49,12 +54,16 @@ require (
gocloud.dev v0.21.0 gocloud.dev v0.21.0
gocloud.dev/secrets/hashivault v0.21.0 gocloud.dev/secrets/hashivault v0.21.0
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad 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/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/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/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/ini.v1 v1.62.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0 // indirect 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.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= 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.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0 h1:kpgPA77kSSbjSs+fWHkPTxQ6J5Z2Qkruo5jfXEkHxNQ= cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 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.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.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 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.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.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.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.28 h1:JVRN7BZgwQ31SQCBwG5QM445+ynJU0ruKu+miFIijYY=
github.com/aws/aws-sdk-go v1.36.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 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/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 v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/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-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-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/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/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= 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.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.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.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/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/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 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/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 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= 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 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= 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= 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.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 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.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/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.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 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-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-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-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/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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 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.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.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.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0= github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I=
github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= 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= 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/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.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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= 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 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.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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.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/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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 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.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.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/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/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= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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-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-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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-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-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-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-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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/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-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-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-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-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= 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.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.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.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.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-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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-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-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-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-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-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/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-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-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-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-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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-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-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-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-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d h1:HV9Z9qMhQEsdlvxNFELgQ11RkMzO3CMkjEySjCtuLes= google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f h1:izedQ6yVIc5mZsRuXzmSreCOlzI0lCU1HpG8yEdMiKw=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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.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.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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.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.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.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.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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 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 package httpd
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@ -64,16 +65,21 @@ func addFolder(w http.ResponseWriter, r *http.Request) {
return return
} }
err = dataprovider.AddFolder(&folder) err = dataprovider.AddFolder(&folder)
if err == nil { 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 {
sendAPIResponse(w, r, err, "", getRespStatus(err)) 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) { func deleteFolderByPath(w http.ResponseWriter, r *http.Request) {
@ -87,15 +93,10 @@ func deleteFolderByPath(w http.ResponseWriter, r *http.Request) {
return return
} }
folder, err := dataprovider.GetFolderByPath(folderPath) err := dataprovider.DeleteFolder(folderPath)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return return
} }
err = dataprovider.DeleteFolder(&folder) sendAPIResponse(w, r, err, "Folder deleted", http.StatusOK)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "Folder deleted", http.StatusOK)
}
} }

View file

@ -112,7 +112,13 @@ func loadData(w http.ResponseWriter, r *http.Request) {
return 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) sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
} }
@ -164,6 +170,33 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, scanQuota
return nil 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 // RestoreUsers restores the specified users
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int) error { func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int) error {
for _, user := range users { for _, user := range users {
@ -176,14 +209,14 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
} }
user.ID = u.ID user.ID = u.ID
err = dataprovider.UpdateUser(&user) 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) logger.Debug(logSender, "", "restoring existing user: %+v, dump file: %#v, error: %v", user, inputFile, err)
if mode == 2 && err == nil { if mode == 2 && err == nil {
disconnectUser(user.Username) disconnectUser(user.Username)
} }
} else { } else {
err = dataprovider.AddUser(&user) 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) logger.Debug(logSender, "", "adding new user: %+v, dump file: %#v, error: %v", user, inputFile, err)
} }
if err != nil { if err != nil {

View file

@ -1,12 +1,12 @@
package httpd package httpd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/go-chi/chi"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/common"
@ -16,11 +16,11 @@ import (
) )
func getUsers(w http.ResponseWriter, r *http.Request) { func getUsers(w http.ResponseWriter, r *http.Request) {
var err error
limit := 100 limit := 100
offset := 0 offset := 0
order := dataprovider.OrderASC order := dataprovider.OrderASC
username := ""
var err error
if _, ok := r.URL.Query()["limit"]; ok { if _, ok := r.URL.Query()["limit"]; ok {
limit, err = strconv.Atoi(r.URL.Query().Get("limit")) limit, err = strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil { if err != nil {
@ -48,10 +48,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
if _, ok := r.URL.Query()["username"]; ok { users, err := dataprovider.GetUsers(limit, offset, order)
username = r.URL.Query().Get("username")
}
users, err := dataprovider.GetUsers(limit, offset, order, username)
if err == nil { if err == nil {
render.JSON(w, r, users) render.JSON(w, r, users)
} else { } else {
@ -59,19 +56,23 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
} }
} }
func getUserByID(w http.ResponseWriter, r *http.Request) { func getUserByUsername(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) 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 { if err != nil {
err = errors.New("Invalid userID") sendAPIResponse(w, r, err, "", getRespStatus(err))
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return return
} }
user, err := dataprovider.GetUserByID(userID) user.HideConfidentialData()
if err == nil { if status != http.StatusOK {
user.HideConfidentialData() ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)
render.JSON(w, r, user) render.JSON(w, r.WithContext(ctx), user)
} else { } 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) err = dataprovider.AddUser(&user)
if err == nil { 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 {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return
} }
renderUser(w, r, user.Username, http.StatusCreated)
} }
func updateUser(w http.ResponseWriter, r *http.Request) { func updateUser(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) var err error
if err != nil {
err = errors.New("Invalid userID") username := getURLParam(r, "username")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
disconnect := 0 disconnect := 0
if _, ok := r.URL.Query()["disconnect"]; ok { if _, ok := r.URL.Query()["disconnect"]; ok {
disconnect, err = strconv.Atoi(r.URL.Query().Get("disconnect")) disconnect, err = strconv.Atoi(r.URL.Query().Get("disconnect"))
@ -146,11 +138,12 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
user, err := dataprovider.GetUserByID(userID) user, err := dataprovider.UserExists(username)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return return
} }
userID := user.ID
currentPermissions := user.Permissions currentPermissions := user.Permissions
currentS3AccessSecret := user.FsConfig.S3Config.AccessSecret currentS3AccessSecret := user.FsConfig.S3Config.AccessSecret
currentAzAccountKey := user.FsConfig.AzBlobConfig.AccountKey currentAzAccountKey := user.FsConfig.AzBlobConfig.AccountKey
@ -170,6 +163,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest) sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return return
} }
user.ID = userID
user.Username = username
user.SetEmptySecretsIfNil() user.SetEmptySecretsIfNil()
// we use new Permissions if passed otherwise the old ones // we use new Permissions if passed otherwise the old ones
if len(user.Permissions) == 0 { if len(user.Permissions) == 0 {
@ -177,40 +172,26 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
} }
updateEncryptedSecrets(&user, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials, currentCryptoPassphrase, updateEncryptedSecrets(&user, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials, currentCryptoPassphrase,
currentSFTPPassword, currentSFTPKey) 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) err = dataprovider.UpdateUser(&user)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
} else { return
sendAPIResponse(w, r, err, "User updated", http.StatusOK) }
if disconnect == 1 { sendAPIResponse(w, r, err, "User updated", http.StatusOK)
disconnectUser(user.Username) if disconnect == 1 {
} disconnectUser(user.Username)
} }
} }
func deleteUser(w http.ResponseWriter, r *http.Request) { func deleteUser(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) username := getURLParam(r, "username")
if err != nil { err := dataprovider.DeleteUser(username)
err = errors.New("Invalid userID")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
user, err := dataprovider.GetUserByID(userID)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return return
} }
err = dataprovider.DeleteUser(&user) sendAPIResponse(w, r, err, "User deleted", http.StatusOK)
if err != nil { disconnectUser(username)
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "User deleted", http.StatusOK)
disconnectUser(user.Username)
}
} }
func disconnectUser(username string) { 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. // 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: // 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 // https://github.com/drakkan/sftpgo/blob/master/httpd/schema/openapi.yaml
// A basic Web interface to manage users and connections is provided too // A basic Web interface to manage users and connections is provided too
package httpd package httpd
import ( import (
"crypto/tls"
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/url"
"path/filepath" "path/filepath"
"runtime" "runtime"
"time" "strings"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -28,29 +25,38 @@ import (
const ( const (
logSender = "httpd" logSender = "httpd"
apiPrefix = "/api/v1" tokenPath = "/api/v2/token"
activeConnectionsPath = "/api/v1/connection" activeConnectionsPath = "/api/v2/connections"
quotaScanPath = "/api/v1/quota_scan" quotaScanPath = "/api/v2/quota-scans"
quotaScanVFolderPath = "/api/v1/folder_quota_scan" quotaScanVFolderPath = "/api/v2/folder-quota-scans"
userPath = "/api/v1/user" userPath = "/api/v2/users"
versionPath = "/api/v1/version" versionPath = "/api/v2/version"
folderPath = "/api/v1/folder" folderPath = "/api/v2/folders"
serverStatusPath = "/api/v1/status" serverStatusPath = "/api/v2/status"
dumpDataPath = "/api/v1/dumpdata" dumpDataPath = "/api/v2/dumpdata"
loadDataPath = "/api/v1/loaddata" loadDataPath = "/api/v2/loaddata"
updateUsedQuotaPath = "/api/v1/quota_update" updateUsedQuotaPath = "/api/v2/quota-update"
updateFolderUsedQuotaPath = "/api/v1/folder_quota_update" updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
defenderBanTime = "/api/v1/defender/ban_time" defenderBanTime = "/api/v2/defender/bantime"
defenderUnban = "/api/v1/defender/unban" defenderUnban = "/api/v2/defender/unban"
defenderScore = "/api/v1/defender/score" defenderScore = "/api/v2/defender/score"
metricsPath = "/metrics" adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/changepwd/admin"
healthzPath = "/healthz"
webBasePath = "/web" webBasePath = "/web"
webLoginPath = "/web/login"
webLogoutPath = "/web/logout"
webUsersPath = "/web/users" webUsersPath = "/web/users"
webUserPath = "/web/user" webUserPath = "/web/user"
webConnectionsPath = "/web/connections" webConnectionsPath = "/web/connections"
webFoldersPath = "/web/folders" webFoldersPath = "/web/folders"
webFolderPath = "/web/folder" webFolderPath = "/web/folder"
webStatusPath = "/web/status" webStatusPath = "/web/status"
webAdminsPath = "/web/admins"
webAdminPath = "/web/admin"
webScanVFolderPath = "/web/folder-quota-scans"
webQuotaScanPath = "/web/quota-scans"
webChangeAdminPwdPath = "/web/changepwd/admin"
webStaticFilesPath = "/static" webStaticFilesPath = "/static"
// MaxRestoreSize defines the max size for the loaddata input file // MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB MaxRestoreSize = 10485760 // 10 MB
@ -59,12 +65,18 @@ const (
) )
var ( var (
router *chi.Mux
backupsPath string backupsPath string
httpAuth common.HTTPAuthProvider
certMgr *common.CertManager 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 { type defenderStatus struct {
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
} }
@ -91,12 +103,6 @@ type Conf struct {
StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"` 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 // 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"` 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 // If files containing a certificate and matching private key for the server are provided the server will expect
// HTTPS connections. // HTTPS connections.
// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a // 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) backupsPath = getConfigPath(c.BackupsPath, configDir)
staticFilesPath := getConfigPath(c.StaticFilesPath, configDir) staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
templatesPath := getConfigPath(c.TemplatesPath, configDir) templatesPath := getConfigPath(c.TemplatesPath, configDir)
enableWebAdmin := len(staticFilesPath) > 0 || len(templatesPath) > 0 enableWebAdmin := staticFilesPath != "" || templatesPath != ""
if backupsPath == "" { if backupsPath == "" {
return fmt.Errorf("Required directory is invalid, backup path %#v", 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", return fmt.Errorf("Required directory is invalid, static file path: %#v template path: %#v",
staticFilesPath, templatesPath) staticFilesPath, templatesPath)
} }
authUserFile := getConfigPath(c.AuthUserFile, configDir)
httpAuth, err = common.NewBasicAuthProvider(authUserFile)
if err != nil {
return err
}
certificateFile := getConfigPath(c.CertificateFile, configDir) certificateFile := getConfigPath(c.CertificateFile, configDir)
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir) certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
if enableWebAdmin { if enableWebAdmin {
@ -148,28 +149,18 @@ func (c Conf) Initialize(configDir string) error {
} else { } else {
logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it") 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 != "" { if certificateFile != "" && certificateKeyFile != "" {
certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender) certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
if err != nil { if err != nil {
return err 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 // ReloadCertificateMgr reloads the certificate manager
@ -202,3 +193,34 @@ func getServicesStatus() ServicesStatus {
} }
return status 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 package httpd
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os" "os"
"path/filepath" "path"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time"
"github.com/go-chi/chi" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/utils" "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) { func TestShouldBind(t *testing.T) {
@ -55,455 +53,6 @@ func TestGetRespStatus(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, respStatus) 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) { func TestGCSWebInvalidFormFile(t *testing.T) {
form := make(url.Values) form := make(url.Values)
form.Set("username", "test_username") form.Set("username", "test_username")
@ -516,170 +65,313 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
assert.EqualError(t, err, http.ErrNotMultipart.Error()) assert.EqualError(t, err, http.ErrNotMultipart.Error())
} }
func TestApiCallsWithBadURL(t *testing.T) { func TestInvalidToken(t *testing.T) {
oldBaseURL := httpBaseURL admin := dataprovider.Admin{
oldAuthUsername := authUsername Username: "admin",
oldAuthPassword := authPassword
SetBaseURLAndCredentials(invalidURL, oldAuthUsername, oldAuthPassword)
folder := vfs.BaseVirtualFolder{
MappedPath: os.TempDir(),
} }
u := dataprovider.User{} errFake := errors.New("fake error")
_, _, err := UpdateUser(u, http.StatusBadRequest, "") asJSON, err := json.Marshal(admin)
assert.Error(t, err) assert.NoError(t, err)
_, err = RemoveUser(u, http.StatusNotFound) req, _ := http.NewRequest(http.MethodPut, path.Join(adminPath, admin.Username), bytes.NewBuffer(asJSON))
assert.Error(t, err) rctx := chi.NewRouteContext()
_, err = RemoveFolder(folder, http.StatusNotFound) rctx.URLParams.Add("username", admin.Username)
assert.Error(t, err) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
_, _, err = GetUsers(1, 0, "", http.StatusBadRequest) req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake))
assert.Error(t, err) rr := httptest.NewRecorder()
_, _, err = GetFolders(1, 0, "", http.StatusBadRequest) updateAdmin(rr, req)
assert.Error(t, err) assert.Equal(t, http.StatusBadRequest, rr.Code)
_, err = UpdateQuotaUsage(u, "", http.StatusNotFound) rr = httptest.NewRecorder()
assert.Error(t, err) deleteAdmin(rr, req)
_, err = UpdateFolderQuotaUsage(folder, "", http.StatusNotFound) assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Error(t, err)
_, err = CloseConnection("non_existent_id", http.StatusNotFound) adminPwd := adminPwd{
assert.Error(t, err) CurrentPassword: "old",
_, _, err = Dumpdata("backup.json", "", http.StatusBadRequest) NewPassword: "new",
assert.Error(t, err) }
_, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusBadRequest) asJSON, err = json.Marshal(adminPwd)
assert.Error(t, err) assert.NoError(t, err)
_, _, err = GetBanTime("", http.StatusBadRequest) req, _ = http.NewRequest(http.MethodPut, "", bytes.NewBuffer(asJSON))
assert.Error(t, err) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
_, _, err = GetScore("", http.StatusBadRequest) req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake))
assert.Error(t, err) rr = httptest.NewRecorder()
SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword) changeAdminPassword(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
adm := getAdminFromToken(req)
assert.Empty(t, adm.Username)
} }
func TestApiCallToNotListeningServer(t *testing.T) { func TestUpdateWebAdminInvalidClaims(t *testing.T) {
oldBaseURL := httpBaseURL server := httpdServer{}
oldAuthUsername := authUsername server.initializeRouter()
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)
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) { func TestCreateTokenError(t *testing.T) {
oldAuthUsername := authUsername server := httpdServer{
oldAuthPassword := authPassword tokenAuth: jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil),
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)
} }
authUserData = append(authUserData, []byte("\"foo\"bar\"\r\n")...) rr := httptest.NewRecorder()
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm) 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) 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) 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) assert.NoError(t, err)
SetBaseURLAndCredentials(httpBaseURL, oldAuthUsername, oldAuthPassword) ctx = jwtauth.NewContext(req.Context(), token, nil)
httpAuth, _ = common.NewBasicAuthProvider("") 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) { 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: info:
title: SFTPGo title: SFTPGo
description: SFTPGo REST API description: SFTPGo REST API
version: 2.3.0 version: 2.4.0
servers: servers:
- url: /api/v1 - url: /api/v2
security: security:
- BasicAuth: [] - BearerAuth: []
paths: paths:
/healthz: /healthz:
get: get:
@ -26,6 +26,29 @@ paths:
schema: schema:
type: string type: string
example: ok 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: /version:
get: get:
tags: tags:
@ -47,7 +70,34 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $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: get:
tags: tags:
- connections - connections
@ -70,7 +120,7 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/connection/{connectionID}: /connections/{connectionID}:
delete: delete:
tags: tags:
- connections - connections
@ -102,7 +152,7 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/defender/ban_time: /defender/bantime:
get: get:
tags: tags:
- defender - defender
@ -197,7 +247,7 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/quota_scan: /quota-scans:
get: get:
tags: tags:
- quota - quota
@ -255,7 +305,7 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/quota_update: /quota-update:
put: put:
tags: tags:
- quota - quota
@ -305,7 +355,7 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/folder_quota_update: /folder-quota-update:
put: put:
tags: tags:
- quota - quota
@ -355,7 +405,7 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/folder_quota_scan: /folder-quota-scans:
get: get:
tags: tags:
- quota - quota
@ -413,7 +463,7 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/folder: /folders:
get: get:
tags: tags:
- folders - folders
@ -484,7 +534,7 @@ paths:
schema: schema:
$ref : '#/components/schemas/BaseVirtualFolder' $ref : '#/components/schemas/BaseVirtualFolder'
responses: responses:
200: 201:
description: successful operation description: successful operation
content: content:
application/json: application/json:
@ -533,7 +583,193 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $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: get:
tags: tags:
- users - users
@ -567,12 +803,6 @@ paths:
- ASC - ASC
- DESC - DESC
example: ASC example: ASC
- in: query
name: username
required: false
description: Filter by username, extact match case sensitive
schema:
type: string
responses: responses:
200: 200:
description: successful operation description: successful operation
@ -604,7 +834,7 @@ paths:
schema: schema:
$ref : '#/components/schemas/User' $ref : '#/components/schemas/User'
responses: responses:
200: 201:
description: successful operation description: successful operation
content: content:
application/json: application/json:
@ -620,21 +850,20 @@ paths:
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
default: default:
$ref: '#/components/responses/DefaultResponse' $ref: '#/components/responses/DefaultResponse'
/user/{userID}: /users/{username}:
get: get:
tags: tags:
- users - users
summary: Find user by ID summary: Find user by username
description: For security reasons the hashed password is omitted in the response description: For security reasons the hashed password is omitted in the response
operationId: get_user_by_id operationId: get_user_by_username
parameters: parameters:
- name: userID - name: username
in: path in: path
description: ID of the user to retrieve description: username of the user to retrieve
required: true required: true
schema: schema:
type: integer type: string
format: int32
responses: responses:
200: 200:
description: successful operation description: successful operation
@ -660,13 +889,12 @@ paths:
summary: Update an existing user summary: Update an existing user
operationId: update_user operationId: update_user
parameters: parameters:
- name: userID - name: username
in: path in: path
description: ID of the user to update description: username of the user to update
required: true required: true
schema: schema:
type: integer type: string
format: int32
- in: query - in: query
name: disconnect name: disconnect
schema: schema:
@ -711,13 +939,12 @@ paths:
summary: Delete an existing user summary: Delete an existing user
operationId: delete_user operationId: delete_user
parameters: parameters:
- name: userID - name: username
in: path in: path
description: ID of the user to delete description: username of the user to delete
required: true required: true
schema: schema:
type: integer type: string
format: int32
responses: responses:
200: 200:
description: successful operation description: successful operation
@ -949,6 +1176,22 @@ components:
minItems: 1 minItems: 1
minProperties: 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 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: LoginMethods:
type: string type: string
enum: enum:
@ -975,14 +1218,12 @@ components:
type: array type: array
items: items:
type: string type: string
nullable: true
description: list of, case insensitive, allowed shell like file patterns. description: list of, case insensitive, allowed shell like file patterns.
example: [ "*.jpg", "a*b?.png" ] example: [ "*.jpg", "a*b?.png" ]
denied_patterns: denied_patterns:
type: array type: array
items: items:
type: string type: string
nullable: true
description: list of, case insensitive, denied shell like file patterns. Denied patterns are evaluated before the allowed ones description: list of, case insensitive, denied shell like file patterns. Denied patterns are evaluated before the allowed ones
example: [ "*.zip" ] example: [ "*.zip" ]
ExtensionsFilter: ExtensionsFilter:
@ -995,14 +1236,12 @@ components:
type: array type: array
items: items:
type: string 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` 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" ] example: [ ".jpg", ".png" ]
denied_extensions: denied_extensions:
type: array type: array
items: items:
type: string type: string
nullable: true
description: list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones description: list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones
example: [ ".zip" ] example: [ ".zip" ]
UserFilters: UserFilters:
@ -1012,44 +1251,37 @@ components:
type: array type: array
items: items:
type: string 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" 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" ] example: [ "192.0.2.0/24", "2001:db8::/32" ]
denied_ip: denied_ip:
type: array type: array
items: items:
type: string type: string
nullable: true
description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones
example: [ "172.16.0.0/16" ] example: [ "172.16.0.0/16" ]
denied_login_methods: denied_login_methods:
type: array type: array
items: items:
$ref: '#/components/schemas/LoginMethods' $ref: '#/components/schemas/LoginMethods'
nullable: true
description: if null or empty any available login method is allowed description: if null or empty any available login method is allowed
denied_protocols: denied_protocols:
type: array type: array
items: items:
$ref: '#/components/schemas/SupportedProtocols' $ref: '#/components/schemas/SupportedProtocols'
nullable: true
description: if null or empty any available protocol is allowed description: if null or empty any available protocol is allowed
file_patterns: file_patterns:
type: array type: array
items: items:
$ref: '#/components/schemas/PatternsFilter' $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 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: file_extensions:
type: array type: array
items: items:
$ref: '#/components/schemas/ExtensionsFilter' $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 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: max_upload_file_size:
type: integer type: integer
format: int64 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: 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 description: Additional restrictions
Secret: Secret:
@ -1106,7 +1338,6 @@ components:
required: required:
- bucket - bucket
- region - region
nullable: true
description: S3 Compatible Object Storage configuration details description: S3 Compatible Object Storage configuration details
GCSConfig: GCSConfig:
type: object type: object
@ -1118,7 +1349,6 @@ components:
$ref: '#/components/schemas/Secret' $ref: '#/components/schemas/Secret'
automatic_credentials: automatic_credentials:
type: integer type: integer
nullable: true
enum: enum:
- 0 - 0
- 1 - 1
@ -1134,7 +1364,6 @@ components:
example: folder/subfolder/ example: folder/subfolder/
required: required:
- bucket - 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 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: AzureBlobFsConfig:
type: object type: object
@ -1171,7 +1400,6 @@ components:
example: folder/subfolder/ example: folder/subfolder/
use_emulator: use_emulator:
type: boolean type: boolean
nullable: true
description: Azure Blob Storage configuration details description: Azure Blob Storage configuration details
CryptFsConfig: CryptFsConfig:
type: object type: object
@ -1253,7 +1481,6 @@ components:
description: Last quota update as unix timestamp in milliseconds description: Last quota update as unix timestamp in milliseconds
users: users:
type: array type: array
nullable: true
items: items:
type: string type: string
description: list of usernames associated with this virtual folder 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 description: expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration
password: password:
type: string 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 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: public_keys:
type: array type: array
items: items:
type: string type: string
nullable: true
description: a password or at least one public key/SSH user certificate are mandatory. description: a password or at least one public key/SSH user certificate are mandatory.
home_dir: home_dir:
type: string type: string
@ -1318,7 +1544,6 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/VirtualFolder' $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 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: uid:
type: integer type: integer
@ -1379,6 +1604,50 @@ components:
additional_info: additional_info:
type: string type: string
description: Free form text field for external systems 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: Transfer:
type: object type: object
properties: properties:
@ -1409,7 +1678,6 @@ components:
description: unique connection identifier description: unique connection identifier
client_version: client_version:
type: string type: string
nullable: true
description: client version description: client version
remote_address: remote_address:
type: string type: string
@ -1420,7 +1688,6 @@ components:
description: connection time as unix timestamp in milliseconds description: connection time as unix timestamp in milliseconds
command: command:
type: string type: string
nullable: true
description: SSH/FTP command or WebDAV method description: SSH/FTP command or WebDAV method
last_activity: last_activity:
type: integer type: integer
@ -1436,7 +1703,6 @@ components:
- DAV - DAV
active_transfers: active_transfers:
type: array type: array
nullable: true
items: items:
$ref : '#/components/schemas/Transfer' $ref : '#/components/schemas/Transfer'
QuotaScan: QuotaScan:
@ -1602,6 +1868,13 @@ components:
score: score:
type: integer type: integer
description: if 0 the host is not listed description: if 0 the host is not listed
PwdChange:
type: object
properties:
current_password:
type: string
new_password:
type: string
ApiResponse: ApiResponse:
type: object type: object
properties: properties:
@ -1610,7 +1883,6 @@ components:
description: message, can be empty description: message, can be empty
error: error:
type: string type: string
nullable: true
description: error description if any description: error description if any
VersionInfo: VersionInfo:
type: object type: object
@ -1626,7 +1898,19 @@ components:
items: items:
type: string 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 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: securitySchemes:
BasicAuth: BasicAuth:
type: http type: http
scheme: basic 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" "html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/go-chi/chi"
"github.com/drakkan/sftpgo/common" "github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/kms"
@ -26,16 +25,23 @@ const (
templateBase = "base.html" templateBase = "base.html"
templateUsers = "users.html" templateUsers = "users.html"
templateUser = "user.html" templateUser = "user.html"
templateAdmins = "admins.html"
templateAdmin = "admin.html"
templateConnections = "connections.html" templateConnections = "connections.html"
templateFolders = "folders.html" templateFolders = "folders.html"
templateFolder = "folder.html" templateFolder = "folder.html"
templateMessage = "message.html" templateMessage = "message.html"
templateStatus = "status.html" templateStatus = "status.html"
templateLogin = "login.html"
templateChangePwd = "changepwd.html"
pageUsersTitle = "Users" pageUsersTitle = "Users"
pageAdminsTitle = "Admins"
pageConnectionsTitle = "Connections" pageConnectionsTitle = "Connections"
pageStatusTitle = "Status" pageStatusTitle = "Status"
pageFoldersTitle = "Folders" pageFoldersTitle = "Folders"
pageChangePwdTitle = "Change password"
page400Title = "Bad request" page400Title = "Bad request"
page403Title = "Forbidden"
page404Title = "Not found" page404Title = "Not found"
page404Body = "The page you are looking for does not exist." page404Body = "The page you are looking for does not exist."
page500Title = "Internal Server Error" page500Title = "Internal Server Error"
@ -50,24 +56,27 @@ var (
) )
type basePage struct { type basePage struct {
Title string Title string
CurrentURL string CurrentURL string
UsersURL string UsersURL string
UserURL string UserURL string
APIUserURL string AdminsURL string
APIConnectionsURL string AdminURL string
APIQuotaScanURL string QuotaScanURL string
ConnectionsURL string ConnectionsURL string
FoldersURL string FoldersURL string
FolderURL string FolderURL string
APIFoldersURL string LogoutURL string
APIFolderQuotaScanURL string ChangeAdminPwdURL string
StatusURL string FolderQuotaScanURL string
UsersTitle string StatusURL string
ConnectionsTitle string UsersTitle string
FoldersTitle string AdminsTitle string
StatusTitle string ConnectionsTitle string
Version string FoldersTitle string
StatusTitle string
Version string
LoggedAdmin *dataprovider.Admin
} }
type usersPage struct { type usersPage struct {
@ -75,6 +84,11 @@ type usersPage struct {
Users []dataprovider.User Users []dataprovider.User
} }
type adminsPage struct {
basePage
Admins []dataprovider.Admin
}
type foldersPage struct { type foldersPage struct {
basePage basePage
Folders []vfs.BaseVirtualFolder Folders []vfs.BaseVirtualFolder
@ -103,6 +117,18 @@ type userPage struct {
IsAdd bool IsAdd bool
} }
type adminPage struct {
basePage
Admin *dataprovider.Admin
Error string
IsAdd bool
}
type changePwdPage struct {
basePage
Error string
}
type folderPage struct { type folderPage struct {
basePage basePage
Folder vfs.BaseVirtualFolder Folder vfs.BaseVirtualFolder
@ -115,6 +141,12 @@ type messagePage struct {
Success string Success string
} }
type loginPage struct {
CurrentURL string
Version string
Error string
}
func loadTemplates(templatesPath string) { func loadTemplates(templatesPath string) {
usersPaths := []string{ usersPaths := []string{
filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateBase),
@ -124,6 +156,18 @@ func loadTemplates(templatesPath string) {
filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateUser), 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{ connectionsPaths := []string{
filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateConnections), filepath.Join(templatesPath, templateConnections),
@ -144,43 +188,57 @@ func loadTemplates(templatesPath string) {
filepath.Join(templatesPath, templateBase), filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateStatus), filepath.Join(templatesPath, templateStatus),
} }
loginPath := []string{
filepath.Join(templatesPath, templateLogin),
}
usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...)) usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...)) userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
adminsTmpl := utils.LoadTemplate(template.ParseFiles(adminsPaths...))
adminTmpl := utils.LoadTemplate(template.ParseFiles(adminPaths...))
connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...)) connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...))
messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...)) messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...))
foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...)) foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...))
folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...)) folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...))
statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...)) statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...))
loginTmpl := utils.LoadTemplate(template.ParseFiles(loginPath...))
changePwdTmpl := utils.LoadTemplate(template.ParseFiles(changePwdPaths...))
templates[templateUsers] = usersTmpl templates[templateUsers] = usersTmpl
templates[templateUser] = userTmpl templates[templateUser] = userTmpl
templates[templateAdmins] = adminsTmpl
templates[templateAdmin] = adminTmpl
templates[templateConnections] = connectionsTmpl templates[templateConnections] = connectionsTmpl
templates[templateMessage] = messageTmpl templates[templateMessage] = messageTmpl
templates[templateFolders] = foldersTmpl templates[templateFolders] = foldersTmpl
templates[templateFolder] = folderTmpl templates[templateFolder] = folderTmpl
templates[templateStatus] = statusTmpl 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{ return basePage{
Title: title, Title: title,
CurrentURL: currentURL, CurrentURL: currentURL,
UsersURL: webUsersPath, UsersURL: webUsersPath,
UserURL: webUserPath, UserURL: webUserPath,
FoldersURL: webFoldersPath, AdminsURL: webAdminsPath,
FolderURL: webFolderPath, AdminURL: webAdminPath,
APIUserURL: userPath, FoldersURL: webFoldersPath,
APIConnectionsURL: activeConnectionsPath, FolderURL: webFolderPath,
APIQuotaScanURL: quotaScanPath, LogoutURL: webLogoutPath,
APIFoldersURL: folderPath, ChangeAdminPwdURL: webChangeAdminPwdPath,
APIFolderQuotaScanURL: quotaScanVFolderPath, QuotaScanURL: webQuotaScanPath,
ConnectionsURL: webConnectionsPath, ConnectionsURL: webConnectionsPath,
StatusURL: webStatusPath, StatusURL: webStatusPath,
UsersTitle: pageUsersTitle, FolderQuotaScanURL: webScanVFolderPath,
ConnectionsTitle: pageConnectionsTitle, UsersTitle: pageUsersTitle,
FoldersTitle: pageFoldersTitle, AdminsTitle: pageAdminsTitle,
StatusTitle: pageStatusTitle, ConnectionsTitle: pageConnectionsTitle,
Version: version.GetAsString(), 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 var errorString string
if len(body) > 0 { if body != "" {
errorString = body + " " errorString = body + " "
} }
if err != nil { if err != nil {
errorString += err.Error() errorString += err.Error()
} }
data := messagePage{ data := messagePage{
basePage: getBasePageData(title, ""), basePage: getBasePageData(title, "", r),
Error: errorString, Error: errorString,
Success: message, Success: message,
} }
@ -208,22 +266,51 @@ func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int
renderTemplate(w, templateMessage, data) renderTemplate(w, templateMessage, data)
} }
func renderInternalServerErrorPage(w http.ResponseWriter, err error) { func renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
renderMessagePage(w, page500Title, page500Body, http.StatusInternalServerError, err, "") renderMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
} }
func renderBadRequestPage(w http.ResponseWriter, err error) { func renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "") renderMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
} }
func renderNotFoundPage(w http.ResponseWriter, err error) { func renderForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "") 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() user.SetEmptySecretsIfNil()
data := userPage{ data := userPage{
basePage: getBasePageData("Add a new user", webUserPath), basePage: getBasePageData("Add a new user", webUserPath, r),
IsAdd: true, IsAdd: true,
Error: error, Error: error,
User: user, User: user,
@ -236,10 +323,10 @@ func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error stri
renderTemplate(w, templateUser, data) 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() user.SetEmptySecretsIfNil()
data := userPage{ 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, IsAdd: false,
Error: error, Error: error,
User: user, User: user,
@ -252,9 +339,9 @@ func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error s
renderTemplate(w, templateUser, data) 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{ data := folderPage{
basePage: getBasePageData("Add a new folder", webFolderPath), basePage: getBasePageData("Add a new folder", webFolderPath, r),
Error: error, Error: error,
Folder: folder, Folder: folder,
} }
@ -571,6 +658,26 @@ func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, er
return fs, nil 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) { func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
var user dataprovider.User var user dataprovider.User
err := r.ParseMultipartForm(maxRequestSize) err := r.ParseMultipartForm(maxRequestSize)
@ -649,6 +756,152 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
return user, err 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) { func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
limit := defaultQueryLimit limit := defaultQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok { 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) users := make([]dataprovider.User, 0, limit)
for { for {
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC, "") u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC)
if err != nil { if err != nil {
renderInternalServerErrorPage(w, err) renderInternalServerErrorPage(w, r, err)
return return
} }
users = append(users, u...) users = append(users, u...)
@ -671,52 +924,44 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
} }
} }
data := usersPage{ data := usersPage{
basePage: getBasePageData(pageUsersTitle, webUsersPath), basePage: getBasePageData(pageUsersTitle, webUsersPath, r),
Users: users, Users: users,
} }
renderTemplate(w, templateUsers, data) renderTemplate(w, templateUsers, data)
} }
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) { func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("cloneFromId") != "" { if r.URL.Query().Get("cloneFrom") != "" {
id, err := strconv.ParseInt(r.URL.Query().Get("cloneFromId"), 10, 64) username := r.URL.Query().Get("cloneFrom")
if err != nil { user, err := dataprovider.UserExists(username)
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(id)
if err == nil { if err == nil {
user.ID = 0 user.ID = 0
user.Username = "" user.Username = ""
if err := user.DecryptSecrets(); err != nil { if err := user.DecryptSecrets(); err != nil {
renderInternalServerErrorPage(w, err) renderInternalServerErrorPage(w, r, err)
return return
} }
renderAddUserPage(w, user, "") renderAddUserPage(w, r, user, "")
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, err) renderNotFoundPage(w, r, err)
} else { } else {
renderInternalServerErrorPage(w, err) renderInternalServerErrorPage(w, r, err)
} }
} else { } else {
user := dataprovider.User{Status: 1} user := dataprovider.User{Status: 1}
renderAddUserPage(w, user, "") renderAddUserPage(w, r, user, "")
} }
} }
func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) { func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) username := getURLParam(r, "username")
if err != nil { user, err := dataprovider.UserExists(username)
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(id)
if err == nil { if err == nil {
renderUpdateUserPage(w, user, "") renderUpdateUserPage(w, r, user, "")
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, err) renderNotFoundPage(w, r, err)
} else { } 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) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
user, err := getUserFromPostFields(r) user, err := getUserFromPostFields(r)
if err != nil { if err != nil {
renderAddUserPage(w, user, err.Error()) renderAddUserPage(w, r, user, err.Error())
return return
} }
err = dataprovider.AddUser(&user) err = dataprovider.AddUser(&user)
if err == nil { if err == nil {
http.Redirect(w, r, webUsersPath, http.StatusSeeOther) http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
} else { } else {
renderAddUserPage(w, user, err.Error()) renderAddUserPage(w, r, user, err.Error())
} }
} }
func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) { func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) username := getURLParam(r, "username")
if err != nil { user, err := dataprovider.UserExists(username)
renderBadRequestPage(w, err)
return
}
user, err := dataprovider.GetUserByID(id)
if _, ok := err.(*dataprovider.RecordNotFoundError); ok { if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, err) renderNotFoundPage(w, r, err)
return return
} else if err != nil { } else if err != nil {
renderInternalServerErrorPage(w, err) renderInternalServerErrorPage(w, r, err)
return return
} }
updatedUser, err := getUserFromPostFields(r) updatedUser, err := getUserFromPostFields(r)
if err != nil { if err != nil {
renderUpdateUserPage(w, user, err.Error()) renderUpdateUserPage(w, r, user, err.Error())
return return
} }
updatedUser.ID = user.ID updatedUser.ID = user.ID
updatedUser.Username = user.Username
updatedUser.SetEmptySecretsIfNil() updatedUser.SetEmptySecretsIfNil()
if len(updatedUser.Password) == 0 { if updatedUser.Password == "" {
updatedUser.Password = user.Password updatedUser.Password = user.Password
} }
updateEncryptedSecrets(&updatedUser, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey, 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) http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
} else { } else {
renderUpdateUserPage(w, user, err.Error()) renderUpdateUserPage(w, r, user, err.Error())
} }
} }
func handleWebGetStatus(w http.ResponseWriter, r *http.Request) { func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
data := statusPage{ data := statusPage{
basePage: getBasePageData(pageStatusTitle, webStatusPath), basePage: getBasePageData(pageStatusTitle, webStatusPath, r),
Status: getServicesStatus(), Status: getServicesStatus(),
} }
renderTemplate(w, templateStatus, data) renderTemplate(w, templateStatus, data)
@ -786,14 +1028,14 @@ func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) { func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
connectionStats := common.Connections.GetStats() connectionStats := common.Connections.GetStats()
data := connectionsPage{ data := connectionsPage{
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath), basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
Connections: connectionStats, Connections: connectionStats,
} }
renderTemplate(w, templateConnections, data) renderTemplate(w, templateConnections, data)
} }
func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) { 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) { func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
@ -801,7 +1043,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
folder := vfs.BaseVirtualFolder{} folder := vfs.BaseVirtualFolder{}
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
renderAddFolderPage(w, folder, err.Error()) renderAddFolderPage(w, r, folder, err.Error())
return return
} }
folder.MappedPath = r.Form.Get("mapped_path") folder.MappedPath = r.Form.Get("mapped_path")
@ -810,7 +1052,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
if err == nil { if err == nil {
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther) http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
} else { } 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 { for {
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, "") f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, "")
if err != nil { if err != nil {
renderInternalServerErrorPage(w, err) renderInternalServerErrorPage(w, r, err)
return return
} }
folders = append(folders, f...) folders = append(folders, f...)
@ -837,7 +1079,7 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
} }
data := foldersPage{ data := foldersPage{
basePage: getBasePageData(pageFoldersTitle, webFoldersPath), basePage: getBasePageData(pageFoldersTitle, webFoldersPath, r),
Folders: folders, Folders: folders,
} }
renderTemplate(w, templateFolders, data) 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="../.." BASE_DIR="../.."
cp ${BASE_DIR}/sftpgo.json . 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|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|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" sftpgo.json
sed -i "s|\"templates\"|\"/usr/share/sftpgo/templates\"|" sftpgo.json sed -i "s|\"templates\"|\"/usr/share/sftpgo/templates\"|" sftpgo.json
@ -61,9 +60,6 @@ contents:
- src: "${BASE_DIR}/init/sftpgo.service" - src: "${BASE_DIR}/init/sftpgo.service"
dst: "/lib/systemd/system/sftpgo.service" dst: "/lib/systemd/system/sftpgo.service"
- src: "./sftpgo_api_cli"
dst: "/usr/bin/sftpgo_api_cli"
- src: "${BASE_DIR}/templates/*" - src: "${BASE_DIR}/templates/*"
dst: "/usr/share/sftpgo/templates/" dst: "/usr/share/sftpgo/templates/"
@ -84,9 +80,6 @@ overrides:
recommends: recommends:
- bash-completion - bash-completion
- mime-support - mime-support
suggests:
- python3-requests
- python3-pygments
scripts: scripts:
postinstall: ../scripts/deb/postinstall.sh postinstall: ../scripts/deb/postinstall.sh
preremove: ../scripts/deb/preremove.sh preremove: ../scripts/deb/preremove.sh
@ -95,7 +88,6 @@ overrides:
recommends: recommends:
- bash-completion - bash-completion
- mailcap - mailcap
# centos 8 has python3-requests, centos 6/7 python-requests
scripts: scripts:
postinstall: ../scripts/rpm/postinstall postinstall: ../scripts/rpm/postinstall
preremove: ../scripts/rpm/preremove preremove: ../scripts/rpm/preremove
@ -112,6 +104,5 @@ tar xvf nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz nfpm
chmod 755 nfpm chmod 755 nfpm
mkdir rpm mkdir rpm
./nfpm -f nfpm.yaml pkg -p rpm -t rpm ./nfpm -f nfpm.yaml pkg -p rpm -t rpm
sed -i "s|env python|env python3|" sftpgo_api_cli
mkdir deb mkdir deb
./nfpm -f nfpm.yaml pkg -p deb -t deb ./nfpm -f nfpm.yaml pkg -p deb -t deb

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -121,7 +121,6 @@
"sslmode": 0, "sslmode": 0,
"connection_string": "", "connection_string": "",
"sql_tables_prefix": "", "sql_tables_prefix": "",
"manage_users": 1,
"track_quota": 2, "track_quota": 2,
"pool_size": 0, "pool_size": 0,
"users_base_dir": "", "users_base_dir": "",
@ -153,7 +152,6 @@
"templates_path": "templates", "templates_path": "templates",
"static_files_path": "static", "static_files_path": "static",
"backups_path": "backups", "backups_path": "backups",
"auth_user_file": "",
"certificate_file": "", "certificate_file": "",
"certificate_key_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" /> <link rel="shortcut icon" href="/static/favicon.ico" />
<!-- Custom fonts for this template--> <!-- 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"> <link href="/static/css/fonts.css" rel="stylesheet">
<!-- Custom styles for this template--> <!-- Custom styles for this template-->
@ -38,6 +39,7 @@
<!-- Page Wrapper --> <!-- Page Wrapper -->
<div id="wrapper"> <div id="wrapper">
{{if .LoggedAdmin.Username}}
<!-- Sidebar --> <!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar"> <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
@ -52,10 +54,10 @@
<!-- Divider --> <!-- Divider -->
<hr class="sidebar-divider my-0"> <hr class="sidebar-divider my-0">
{{ if .LoggedAdmin.HasPermission "view_users"}}
<li class="nav-item {{if eq .CurrentURL .UsersURL}}active{{end}}"> <li class="nav-item {{if eq .CurrentURL .UsersURL}}active{{end}}">
<a class="nav-link" href="{{.UsersURL}}"> <a class="nav-link" href="{{.UsersURL}}">
<i class="fas fa-fw fa-user"></i> <i class="fas fa-users"></i>
<span>{{.UsersTitle}}</span></a> <span>{{.UsersTitle}}</span></a>
</li> </li>
@ -64,18 +66,31 @@
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
<span>{{.FoldersTitle}}</span></a> <span>{{.FoldersTitle}}</span></a>
</li> </li>
{{end}}
{{ if .LoggedAdmin.HasPermission "view_conns"}}
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}"> <li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
<a class="nav-link" href="{{.ConnectionsURL}}"> <a class="nav-link" href="{{.ConnectionsURL}}">
<i class="fas fa-exchange-alt"></i> <i class="fas fa-exchange-alt"></i>
<span>{{.ConnectionsTitle}}</span></a> <span>{{.ConnectionsTitle}}</span></a>
</li> </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}}"> <li class="nav-item {{if eq .CurrentURL .StatusURL}}active{{end}}">
<a class="nav-link" href="{{.StatusURL}}"> <a class="nav-link" href="{{.StatusURL}}">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
<span>{{.StatusTitle}}</span></a> <span>{{.StatusTitle}}</span></a>
</li> </li>
{{end}}
<!-- Divider --> <!-- Divider -->
<hr class="sidebar-divider d-none d-md-block"> <hr class="sidebar-divider d-none d-md-block">
@ -87,6 +102,7 @@
</ul> </ul>
<!-- End of Sidebar --> <!-- End of Sidebar -->
{{end}}
<!-- Content Wrapper --> <!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column"> <div id="content-wrapper" class="d-flex flex-column">
@ -94,11 +110,43 @@
<!-- Main Content --> <!-- Main Content -->
<div id="content"> <div id="content">
{{if .LoggedAdmin.Username}}
<!-- Topbar --> <!-- 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> </nav>
<!-- End of Topbar --> <!-- End of Topbar -->
{{end}}
<!-- Begin Page Content --> <!-- Begin Page Content -->
<div class="container-fluid"> <div class="container-fluid">
@ -110,7 +158,7 @@
</div> </div>
<!-- End of Main Content --> <!-- End of Main Content -->
{{if .LoggedAdmin.Username}}
<!-- Footer --> <!-- Footer -->
<footer class="sticky-footer bg-white"> <footer class="sticky-footer bg-white">
<div class="container my-auto"> <div class="container my-auto">
@ -120,6 +168,7 @@
</div> </div>
</footer> </footer>
<!-- End of Footer --> <!-- End of Footer -->
{{end}}
</div> </div>
<!-- End of Content Wrapper --> <!-- End of Content Wrapper -->
@ -132,6 +181,26 @@
<i class="fas fa-angle-up"></i> <i class="fas fa-angle-up"></i>
</a> </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}} {{block "dialog" .}}{{end}}
<!-- Bootstrap core JavaScript--> <!-- 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() { function disconnectAction() {
var table = $('#dataTable').DataTable(); var table = $('#dataTable').DataTable();
table.button(0).enable(false); table.button('disconnect:name').enable(false);
var connectionID = table.row({ selected: true }).data()[0]; var connectionID = table.row({ selected: true }).data()[0];
var path = '{{.APIConnectionsURL}}' + "/" + connectionID; var path = '{{.ConnectionsURL}}' + "/" + connectionID;
$('#disconnectModal').modal('hide'); $('#disconnectModal').modal('hide');
$.ajax({ $.ajax({
url: path, url: path,
@ -101,12 +101,12 @@
timeout: 15000, timeout: 15000,
success: function (result) { success: function (result) {
setTimeout(function () { setTimeout(function () {
table.button(0).enable(true); table.button('disconnect:name').enable(true);
window.location.href = '{{.ConnectionsURL}}'; window.location.href = '{{.ConnectionsURL}}';
}, 1000); }, 1000);
}, },
error: function ($xhr, textStatus, errorThrown) { error: function ($xhr, textStatus, errorThrown) {
table.button(0).enable(true); table.button('disconnect:name').enable(true);
var txt = "Unable to close the selected connection"; var txt = "Unable to close the selected connection";
if ($xhr) { if ($xhr) {
var json = $xhr.responseJSON; var json = $xhr.responseJSON;
@ -126,6 +126,7 @@
$(document).ready(function () { $(document).ready(function () {
$.fn.dataTable.ext.buttons.disconnect = { $.fn.dataTable.ext.buttons.disconnect = {
text: 'Disconnect', text: 'Disconnect',
name: 'disconnect',
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
$('#disconnectModal').modal('show'); $('#disconnectModal').modal('show');
}, },
@ -138,9 +139,7 @@
"<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
select: true, select: true,
buttons: [ buttons: [],
'disconnect'
],
"columnDefs": [ "columnDefs": [
{ {
"targets": [0], "targets": [0],
@ -152,10 +151,14 @@
"order": [[1, 'asc']] "order": [[1, 'asc']]
}); });
{{if .LoggedAdmin.HasPermission "close_conns"}}
table.button().add(0,'disconnect');
table.on('select deselect', function () { table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count(); var selectedRows = table.rows({ selected: true }).count();
table.button(0).enable(selectedRows == 1); table.button('disconnect:name').enable(selectedRows == 1);
}); });
{{end}}
}); });
</script> </script>
{{end}} {{end}}

View file

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

View file

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

View file

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

View file

@ -26,12 +26,15 @@ import (
"strings" "strings"
"time" "time"
"github.com/rs/xid"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/logger" "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 // IsStringInSlice searches a string in a slice and returns true if the string is found
func IsStringInSlice(obj string, list []string) bool { func IsStringInSlice(obj string, list []string) bool {
@ -383,6 +386,22 @@ func createDirPathIfMissing(file string, perm os.FileMode) error {
return nil 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 // HTTPListenAndServe is a wrapper for ListenAndServe that support both tcp
// and Unix-domain sockets // and Unix-domain sockets
func HTTPListenAndServe(srv *http.Server, address string, port int, isTLS bool, logSender string) error { 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) _, ok = dataprovider.GetCachedWebDAVUser(username)
assert.True(t, ok) assert.True(t, ok)
// cache is invalidated after user deletion // cache is invalidated after user deletion
err = dataprovider.DeleteUser(&user) err = dataprovider.DeleteUser(user.Username)
assert.NoError(t, err) assert.NoError(t, err)
_, ok = dataprovider.GetCachedWebDAVUser(username) _, ok = dataprovider.GetCachedWebDAVUser(username)
assert.False(t, ok) assert.False(t, ok)
@ -1164,13 +1164,13 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username) _, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
assert.True(t, ok) assert.True(t, ok)
err = dataprovider.DeleteUser(&user1) err = dataprovider.DeleteUser(user1.Username)
assert.NoError(t, err) assert.NoError(t, err)
err = dataprovider.DeleteUser(&user2) err = dataprovider.DeleteUser(user2.Username)
assert.NoError(t, err) assert.NoError(t, err)
err = dataprovider.DeleteUser(&user3) err = dataprovider.DeleteUser(user3.Username)
assert.NoError(t, err) assert.NoError(t, err)
err = dataprovider.DeleteUser(&user4) err = dataprovider.DeleteUser(user4.Username)
assert.NoError(t, err) assert.NoError(t, err)
err = os.RemoveAll(u.GetHomeDir()) err = os.RemoveAll(u.GetHomeDir())

View file

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