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
2
.github/workflows/development.yml
vendored
|
@ -98,13 +98,11 @@ jobs:
|
|||
if: startsWith(matrix.os, 'windows-') != true
|
||||
run: |
|
||||
mkdir -p output/{bash_completion,zsh_completion}
|
||||
mkdir -p output/examples/rest-api-cli
|
||||
cp sftpgo output/
|
||||
cp sftpgo.json output/
|
||||
cp -r templates output/
|
||||
cp -r static output/
|
||||
cp -r init output/
|
||||
cp examples/rest-api-cli/sftpgo_api_cli output/examples/rest-api-cli/
|
||||
./sftpgo gen completion bash > output/bash_completion/sftpgo
|
||||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
|
|
22
.github/workflows/release.yml
vendored
|
@ -94,12 +94,6 @@ jobs:
|
|||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Set up Python
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Build for Linux/macOS
|
||||
if: startsWith(matrix.os, 'windows-') != true
|
||||
run: go build -ldflags "-s -w -X github.com/drakkan/sftpgo/version.commit=`git describe --always --dirty` -X github.com/drakkan/sftpgo/version.date=`date -u +%FT%TZ`" -o sftpgo
|
||||
|
@ -136,15 +130,6 @@ jobs:
|
|||
env:
|
||||
MATRIX_OS: ${{ matrix.os }}
|
||||
|
||||
- name: Build REST API CLI for Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
pip install requests
|
||||
pip install pygments
|
||||
pip install pyinstaller
|
||||
pyinstaller --hidden-import="pkg_resources.py2_warn" --noupx --onefile examples\rest-api-cli\sftpgo_api_cli
|
||||
|
||||
- name: Gather cross build info
|
||||
id: cross_info
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
|
@ -170,7 +155,7 @@ jobs:
|
|||
- name: Prepare Release for Linux/macOS
|
||||
if: startsWith(matrix.os, 'windows-') != true
|
||||
run: |
|
||||
mkdir -p output/{init,examples/rest-api-cli,sqlite,bash_completion,zsh_completion}
|
||||
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
|
||||
echo "For documentation please take a look here:" > output/README.txt
|
||||
echo "" >> output/README.txt
|
||||
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
|
||||
|
@ -190,7 +175,6 @@ jobs:
|
|||
./sftpgo gen completion zsh > output/zsh_completion/_sftpgo
|
||||
./sftpgo gen man -d output/man/man1
|
||||
gzip output/man/man1/*
|
||||
cp examples/rest-api-cli/sftpgo_api_cli output/examples/rest-api-cli/
|
||||
if [ $OS == 'linux' ]
|
||||
then
|
||||
cp -r output output_arm64
|
||||
|
@ -254,7 +238,6 @@ jobs:
|
|||
copy .\sftpgo.exe .\output
|
||||
copy .\sftpgo.json .\output
|
||||
copy .\sftpgo.db .\output
|
||||
copy .\dist\sftpgo_api_cli.exe .\output
|
||||
copy .\LICENSE .\output\LICENSE.txt
|
||||
mkdir output\templates
|
||||
xcopy .\templates .\output\templates\ /E
|
||||
|
@ -268,11 +251,10 @@ jobs:
|
|||
- name: Prepare Portable Release for Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
mkdir win-portable\examples\rest-api-cli
|
||||
mkdir win-portable
|
||||
copy .\sftpgo.exe .\win-portable
|
||||
copy .\sftpgo.json .\win-portable
|
||||
copy .\sftpgo.db .\win-portable
|
||||
copy .\dist\sftpgo_api_cli.exe .\win-portable\examples\rest-api-cli
|
||||
copy .\LICENSE .\win-portable\LICENSE.txt
|
||||
mkdir win-portable\templates
|
||||
xcopy .\templates .\win-portable\templates\ /E
|
||||
|
|
|
@ -46,7 +46,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy
|
|||
- Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP/FTP/WebDAV service without losing the information about the client's address.
|
||||
- [REST API](./docs/rest-api.md) for users and folders management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
|
||||
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
|
||||
- Easy [migration](./examples/rest-api-cli#convert-users-from-other-stores) from Linux system user accounts.
|
||||
- Easy [migration](./examples/convertusers) from Linux system user accounts.
|
||||
- [Portable mode](./docs/portable-mode.md): a convenient way to share a single directory on demand.
|
||||
- [SFTP subsystem mode](./docs/sftp-subsystem.md): you can use SFTPGo as OpenSSH's SFTP subsystem.
|
||||
- Performance analysis using built-in [profiler](./docs/profiling.md).
|
||||
|
@ -147,7 +147,6 @@ After starting SFTPGo you can manage users and folders using:
|
|||
|
||||
- the [web based administration interface](./docs/web-admin.md)
|
||||
- the [REST API](./docs/rest-api.md)
|
||||
- the sample [REST API CLI](./examples/rest-api-cli)
|
||||
|
||||
To support embedded data providers like `bolt` and `SQLite` we can't have a CLI that directly write users and folders to the data provider, we always have to use the REST API.
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ Command-line flags should be specified in the Subsystem declaration.
|
|||
dataProviderConf.PreferDatabaseCredentials = true
|
||||
}
|
||||
config.SetProviderConf(dataProviderConf)
|
||||
err = dataprovider.Initialize(dataProviderConf, configDir)
|
||||
err = dataprovider.Initialize(dataProviderConf, configDir, false)
|
||||
if err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -179,7 +179,7 @@ func initializeDataprovider(trackQuota int) (string, error) {
|
|||
if trackQuota >= 0 && trackQuota <= 2 {
|
||||
cfg.Config.TrackQuota = trackQuota
|
||||
}
|
||||
return cfg.Config.Driver, dataprovider.Initialize(cfg.Config, configDir)
|
||||
return cfg.Config.Driver, dataprovider.Initialize(cfg.Config, configDir, true)
|
||||
}
|
||||
|
||||
func closeDataprovider() error {
|
||||
|
|
|
@ -1057,10 +1057,10 @@ func TestHasSpace(t *testing.T) {
|
|||
quotaResult = c.HasSpace(true, "/vdir/file1")
|
||||
assert.False(t, quotaResult.HasSpace)
|
||||
|
||||
err = dataprovider.DeleteUser(&user)
|
||||
err = dataprovider.DeleteUser(user.Username)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = dataprovider.DeleteFolder(&folder)
|
||||
err = dataprovider.DeleteFolder(folder.MappedPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -1133,7 +1133,7 @@ func TestUpdateQuotaMoveVFolders(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, folder2.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), folder2.UsedQuotaSize)
|
||||
user, err = dataprovider.GetUserByID(user.ID)
|
||||
user, err = dataprovider.UserExists(user.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), user.UsedQuotaSize)
|
||||
|
@ -1143,16 +1143,16 @@ func TestUpdateQuotaMoveVFolders(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, folder2.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(200), folder2.UsedQuotaSize)
|
||||
user, err = dataprovider.GetUserByID(user.ID)
|
||||
user, err = dataprovider.UserExists(user.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), user.UsedQuotaSize)
|
||||
|
||||
err = dataprovider.DeleteUser(&user)
|
||||
err = dataprovider.DeleteUser(user.Username)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.DeleteFolder(&folder1)
|
||||
err = dataprovider.DeleteFolder(folder1.MappedPath)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.DeleteFolder(&folder2)
|
||||
err = dataprovider.DeleteFolder(folder2.MappedPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -293,8 +293,8 @@ func (t *BaseTransfer) HandleThrottle() {
|
|||
if wantedBandwidth > 0 {
|
||||
// real and wanted elapsed as milliseconds, bytes as kilobytes
|
||||
realElapsed := time.Since(t.start).Nanoseconds() / 1000000
|
||||
// trasferredBytes / 1000 = KB/s, we multiply for 1000 to get milliseconds
|
||||
wantedElapsed := 1000 * (trasferredBytes / 1000) / wantedBandwidth
|
||||
// trasferredBytes / 1024 = KB/s, we multiply for 1000 to get milliseconds
|
||||
wantedElapsed := 1000 * (trasferredBytes / 1024) / wantedBandwidth
|
||||
if wantedElapsed > realElapsed {
|
||||
toSleep := time.Duration(wantedElapsed - realElapsed)
|
||||
time.Sleep(toSleep * time.Millisecond)
|
||||
|
|
|
@ -57,8 +57,8 @@ func TestTransferThrottling(t *testing.T) {
|
|||
}
|
||||
fs := vfs.NewOsFs("", os.TempDir(), nil)
|
||||
testFileSize := int64(131072)
|
||||
wantedUploadElapsed := 1000 * (testFileSize / 1000) / u.UploadBandwidth
|
||||
wantedDownloadElapsed := 1000 * (testFileSize / 1000) / u.DownloadBandwidth
|
||||
wantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth
|
||||
wantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth
|
||||
// some tolerance
|
||||
wantedUploadElapsed -= wantedDownloadElapsed / 10
|
||||
wantedDownloadElapsed -= wantedDownloadElapsed / 10
|
||||
|
|
|
@ -175,7 +175,6 @@ func Init() {
|
|||
Password: "",
|
||||
ConnectionString: "",
|
||||
SQLTablesPrefix: "",
|
||||
ManageUsers: 1,
|
||||
SSLMode: 0,
|
||||
TrackQuota: 1,
|
||||
PoolSize: 0,
|
||||
|
@ -208,7 +207,6 @@ func Init() {
|
|||
TemplatesPath: "templates",
|
||||
StaticFilesPath: "static",
|
||||
BackupsPath: "backups",
|
||||
AuthUserFile: "",
|
||||
CertificateFile: "",
|
||||
CertificateKeyFile: "",
|
||||
},
|
||||
|
@ -749,7 +747,6 @@ func setViperDefaults() {
|
|||
viper.SetDefault("data_provider.sslmode", globalConf.ProviderConf.SSLMode)
|
||||
viper.SetDefault("data_provider.connection_string", globalConf.ProviderConf.ConnectionString)
|
||||
viper.SetDefault("data_provider.sql_tables_prefix", globalConf.ProviderConf.SQLTablesPrefix)
|
||||
viper.SetDefault("data_provider.manage_users", globalConf.ProviderConf.ManageUsers)
|
||||
viper.SetDefault("data_provider.track_quota", globalConf.ProviderConf.TrackQuota)
|
||||
viper.SetDefault("data_provider.pool_size", globalConf.ProviderConf.PoolSize)
|
||||
viper.SetDefault("data_provider.users_base_dir", globalConf.ProviderConf.UsersBaseDir)
|
||||
|
@ -773,7 +770,6 @@ func setViperDefaults() {
|
|||
viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath)
|
||||
viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
|
||||
viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath)
|
||||
viper.SetDefault("httpd.auth_user_file", globalConf.HTTPDConfig.AuthUserFile)
|
||||
viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile)
|
||||
viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile)
|
||||
viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout)
|
||||
|
|
228
dataprovider/admin.go
Normal file
|
@ -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}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -24,8 +23,9 @@ const (
|
|||
|
||||
var (
|
||||
usersBucket = []byte("users")
|
||||
usersIDIdxBucket = []byte("users_id_idx")
|
||||
//usersIDIdxBucket = []byte("users_id_idx")
|
||||
foldersBucket = []byte("folders")
|
||||
adminsBucket = []byte("admins")
|
||||
dbVersionBucket = []byte("db_version")
|
||||
dbVersionKey = []byte("version")
|
||||
)
|
||||
|
@ -63,10 +63,6 @@ func initializeBoltProvider(basePath string) error {
|
|||
providerLog(logger.LevelWarn, "error creating users bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
_, e := tx.CreateBucketIfNotExists(usersIDIdxBucket)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err)
|
||||
return err
|
||||
|
@ -76,7 +72,15 @@ func initializeBoltProvider(basePath string) error {
|
|||
return e
|
||||
})
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error creating username idx bucket: %v", err)
|
||||
providerLog(logger.LevelWarn, "error creating folders bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
_, e := tx.CreateBucketIfNotExists(adminsBucket)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error creating admins bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
|
@ -87,19 +91,19 @@ func initializeBoltProvider(basePath string) error {
|
|||
providerLog(logger.LevelWarn, "error creating database version bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
provider = BoltProvider{dbHandle: dbHandle}
|
||||
provider = &BoltProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
providerLog(logger.LevelWarn, "error creating bolt key/value store handler: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p BoltProvider) checkAvailability() error {
|
||||
func (p *BoltProvider) checkAvailability() error {
|
||||
_, err := getBoltDatabaseVersion(p.dbHandle)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
func (p *BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
var user User
|
||||
if password == "" {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
|
@ -112,7 +116,17 @@ func (p BoltProvider) validateUserAndPass(username, password, ip, protocol strin
|
|||
return checkUserAndPass(user, password, ip, protocol)
|
||||
}
|
||||
|
||||
func (p BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
|
||||
func (p *BoltProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
|
||||
admin, err := p.adminExists(username)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
|
||||
return admin, err
|
||||
}
|
||||
err = admin.checkUserAndPass(password, ip)
|
||||
return admin, err
|
||||
}
|
||||
|
||||
func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
|
||||
var user User
|
||||
if len(pubKey) == 0 {
|
||||
return user, "", errors.New("Credentials cannot be null or empty")
|
||||
|
@ -125,36 +139,9 @@ func (p BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (Use
|
|||
return checkUserAndPubKey(user, pubKey)
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUserByID(ID int64) (User, error) {
|
||||
var user User
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(ID)
|
||||
username := idxBucket.Get(userIDAsBytes)
|
||||
if username == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)}
|
||||
}
|
||||
u := bucket.Get(username)
|
||||
if u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username %#v and ID: %v does not exist", string(username), ID)}
|
||||
}
|
||||
folderBucket, err := getFolderBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err = joinUserAndFolders(u, folderBucket)
|
||||
return err
|
||||
})
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateLastLogin(username string) error {
|
||||
func (p *BoltProvider) updateLastLogin(username string) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -182,9 +169,9 @@ func (p BoltProvider) updateLastLogin(username string) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -216,7 +203,7 @@ func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64,
|
|||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
func (p *BoltProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
user, err := p.userExists(username)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "unable to get quota for user %v error: %v", username, err)
|
||||
|
@ -225,10 +212,173 @@ func (p BoltProvider) getUsedQuota(username string) (int, int64, error) {
|
|||
return user.UsedQuotaFiles, user.UsedQuotaSize, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) userExists(username string) (User, error) {
|
||||
func (p *BoltProvider) adminExists(username string) (Admin, error) {
|
||||
var admin Admin
|
||||
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, err := getAdminBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a := bucket.Get([]byte(username))
|
||||
if a == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", username)}
|
||||
}
|
||||
return json.Unmarshal(a, &admin)
|
||||
})
|
||||
|
||||
return admin, err
|
||||
}
|
||||
|
||||
func (p *BoltProvider) addAdmin(admin *Admin) error {
|
||||
err := admin.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := getAdminBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a := bucket.Get([]byte(admin.Username)); a != nil {
|
||||
return fmt.Errorf("admin %v already exists", admin.Username)
|
||||
}
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
admin.ID = int64(id)
|
||||
buf, err := json.Marshal(admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(admin.Username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) updateAdmin(admin *Admin) error {
|
||||
err := admin.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := getAdminBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var a []byte
|
||||
|
||||
if a = bucket.Get([]byte(admin.Username)); a == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", admin.Username)}
|
||||
}
|
||||
var oldAdmin Admin
|
||||
err = json.Unmarshal(a, &oldAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
admin.ID = oldAdmin.ID
|
||||
buf, err := json.Marshal(admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(admin.Username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) deleteAdmin(admin *Admin) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := getAdminBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bucket.Get([]byte(admin.Username)) == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("admin %v does not exist", admin.Username)}
|
||||
}
|
||||
|
||||
return bucket.Delete([]byte(admin.Username))
|
||||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
|
||||
admins := make([]Admin, 0, limit)
|
||||
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, err := getAdminBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := bucket.Cursor()
|
||||
itNum := 0
|
||||
if order == OrderASC {
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
var admin Admin
|
||||
err = json.Unmarshal(v, &admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
admin.HideConfidentialData()
|
||||
admins = append(admins, admin)
|
||||
if len(admins) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
var admin Admin
|
||||
err = json.Unmarshal(v, &admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
admin.HideConfidentialData()
|
||||
admins = append(admins, admin)
|
||||
if len(admins) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
return admins, err
|
||||
}
|
||||
|
||||
func (p *BoltProvider) dumpAdmins() ([]Admin, error) {
|
||||
admins := make([]Admin, 0, 30)
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, err := getAdminBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var admin Admin
|
||||
err = json.Unmarshal(v, &admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
admins = append(admins, admin)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
return admins, err
|
||||
}
|
||||
|
||||
func (p *BoltProvider) userExists(username string) (User, error) {
|
||||
var user User
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -246,13 +396,13 @@ func (p BoltProvider) userExists(username string) (User, error) {
|
|||
return user, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) addUser(user *User) error {
|
||||
func (p *BoltProvider) addUser(user *User) error {
|
||||
err := validateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -282,22 +432,17 @@ func (p BoltProvider) addUser(user *User) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bucket.Put([]byte(user.Username), buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(user.ID)
|
||||
return idxBucket.Put(userIDAsBytes, []byte(user.Username))
|
||||
return bucket.Put([]byte(user.Username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateUser(user *User) error {
|
||||
func (p *BoltProvider) updateUser(user *User) error {
|
||||
err := validateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -339,9 +484,9 @@ func (p BoltProvider) updateUser(user *User) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) deleteUser(user *User) error {
|
||||
func (p *BoltProvider) deleteUser(user *User) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -357,23 +502,18 @@ func (p BoltProvider) deleteUser(user *User) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
userIDAsBytes := itob(user.ID)
|
||||
userName := idxBucket.Get(userIDAsBytes)
|
||||
if userName == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("user with id %v does not exist", user.ID)}
|
||||
exists := bucket.Get([]byte(user.Username))
|
||||
if exists == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("user %#v does not exist", user.Username)}
|
||||
}
|
||||
err = bucket.Delete(userName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return idxBucket.Delete(userIDAsBytes)
|
||||
return bucket.Delete([]byte(user.Username))
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) dumpUsers() ([]User, error) {
|
||||
func (p *BoltProvider) dumpUsers() ([]User, error) {
|
||||
users := make([]User, 0, 100)
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -398,35 +538,14 @@ func (p BoltProvider) dumpUsers() ([]User, error) {
|
|||
return users, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUserWithUsername(username string) ([]User, error) {
|
||||
users := []User{}
|
||||
var user User
|
||||
user, err := p.userExists(username)
|
||||
if err == nil {
|
||||
user.HideConfidentialData()
|
||||
users = append(users, user)
|
||||
return users, nil
|
||||
}
|
||||
if _, ok := err.(*RecordNotFoundError); ok {
|
||||
err = nil
|
||||
}
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
func (p *BoltProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
||||
users := make([]User, 0, limit)
|
||||
var err error
|
||||
if limit <= 0 {
|
||||
return users, err
|
||||
}
|
||||
if len(username) > 0 {
|
||||
if offset == 0 {
|
||||
return p.getUserWithUsername(username)
|
||||
}
|
||||
return users, err
|
||||
}
|
||||
err = p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -472,7 +591,7 @@ func (p BoltProvider) getUsers(limit int, offset int, order string, username str
|
|||
return users, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, 50)
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, err := getFolderBucket(tx)
|
||||
|
@ -493,7 +612,7 @@ func (p BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
|||
return folders, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *BoltProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, limit)
|
||||
var err error
|
||||
if limit <= 0 {
|
||||
|
@ -554,7 +673,7 @@ func (p BoltProvider) getFolders(limit, offset int, order, folderPath string) ([
|
|||
return folders, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error) {
|
||||
func (p *BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error) {
|
||||
var folder vfs.BaseVirtualFolder
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, err := getFolderBucket(tx)
|
||||
|
@ -567,7 +686,7 @@ func (p BoltProvider) getFolderByPath(name string) (vfs.BaseVirtualFolder, error
|
|||
return folder, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
err := validateFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -585,13 +704,13 @@ func (p BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := getFolderBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usersBucket, _, err := getBuckets(tx)
|
||||
usersBucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -635,7 +754,7 @@ func (p BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := getFolderBucket(tx)
|
||||
if err != nil {
|
||||
|
@ -666,7 +785,7 @@ func (p BoltProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd
|
|||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
func (p *BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
folder, err := p.getFolderByPath(mappedPath)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "unable to get quota for folder %#v error: %v", mappedPath, err)
|
||||
|
@ -675,20 +794,20 @@ func (p BoltProvider) getUsedFolderQuota(mappedPath string) (int, int64, error)
|
|||
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) close() error {
|
||||
func (p *BoltProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p BoltProvider) reloadConfig() error {
|
||||
func (p *BoltProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeDatabase does nothing, no initilization is needed for bolt provider
|
||||
func (p BoltProvider) initializeDatabase() error {
|
||||
func (p *BoltProvider) initializeDatabase() error {
|
||||
return ErrNoInitRequired
|
||||
}
|
||||
|
||||
func (p BoltProvider) migrateDatabase() error {
|
||||
func (p *BoltProvider) migrateDatabase() error {
|
||||
dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -718,7 +837,7 @@ func (p BoltProvider) migrateDatabase() error {
|
|||
}
|
||||
}
|
||||
|
||||
func (p BoltProvider) revertDatabase(targetVersion int) error {
|
||||
func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
||||
dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -762,16 +881,12 @@ func updateBoltDatabaseFromV4(dbHandle *bolt.DB) error {
|
|||
return updateDatabaseFrom4To5(dbHandle)
|
||||
}
|
||||
|
||||
// itob returns an 8-byte big endian representation of v.
|
||||
func itob(v int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(v))
|
||||
return b
|
||||
}
|
||||
|
||||
func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) {
|
||||
var user User
|
||||
err := json.Unmarshal(u, &user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if len(user.VirtualFolders) > 0 {
|
||||
var folders []vfs.VirtualFolder
|
||||
for _, folder := range user.VirtualFolders {
|
||||
|
@ -872,7 +987,7 @@ func removeUserFromFolderMapping(folder vfs.VirtualFolder, user *User, bucket *b
|
|||
|
||||
func updateV4BoltCompatUser(dbHandle *bolt.DB, user compatUserV4) error {
|
||||
return dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -893,7 +1008,7 @@ func updateV4BoltUser(dbHandle *bolt.DB, user User) error {
|
|||
return err
|
||||
}
|
||||
return dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -908,14 +1023,23 @@ func updateV4BoltUser(dbHandle *bolt.DB, user User) error {
|
|||
})
|
||||
}
|
||||
|
||||
func getBuckets(tx *bolt.Tx) (*bolt.Bucket, *bolt.Bucket, error) {
|
||||
func getAdminBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||
var err error
|
||||
|
||||
bucket := tx.Bucket(adminsBucket)
|
||||
if bucket == nil {
|
||||
err = errors.New("unable to find admin bucket, bolt database structure not correcly defined")
|
||||
}
|
||||
return bucket, err
|
||||
}
|
||||
|
||||
func getUsersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||
var err error
|
||||
bucket := tx.Bucket(usersBucket)
|
||||
idxBucket := tx.Bucket(usersIDIdxBucket)
|
||||
if bucket == nil || idxBucket == nil {
|
||||
err = fmt.Errorf("unable to find required buckets, bolt database structure not correcly defined")
|
||||
if bucket == nil {
|
||||
err = errors.New("unable to find required buckets, bolt database structure not correcly defined")
|
||||
}
|
||||
return bucket, idxBucket, err
|
||||
return bucket, err
|
||||
}
|
||||
|
||||
func getFolderBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||
|
@ -954,7 +1078,7 @@ func updateDatabaseFrom2To3(dbHandle *bolt.DB) error {
|
|||
providerLog(logger.LevelInfo, "updating bolt database version: 2 -> 3")
|
||||
users := []User{}
|
||||
err := dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1011,7 +1135,7 @@ func updateDatabaseFrom3To4(dbHandle *bolt.DB) error {
|
|||
foldersToScan := []string{}
|
||||
users := []userCompactVFolders{}
|
||||
err := dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1075,7 +1199,7 @@ func downgradeBoltDatabaseFrom5To4(dbHandle *bolt.DB) error {
|
|||
providerLog(logger.LevelInfo, "downgrading bolt database version: 5 -> 4")
|
||||
users := []compatUserV4{}
|
||||
err := dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1116,7 +1240,7 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error {
|
|||
providerLog(logger.LevelInfo, "updating bolt database version: 4 -> 5")
|
||||
users := []User{}
|
||||
err := dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1154,13 +1278,13 @@ func updateDatabaseFrom4To5(dbHandle *bolt.DB) error {
|
|||
func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) {
|
||||
usernames := []string{}
|
||||
err := dbHandle.View(func(tx *bolt.Tx) error {
|
||||
_, idxBucket, err := getBuckets(tx)
|
||||
bucket, err := getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := idxBucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
usernames = append(usernames, string(v))
|
||||
cursor := bucket.Cursor()
|
||||
for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() {
|
||||
usernames = append(usernames, string(k))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -62,7 +63,7 @@ const (
|
|||
MemoryDataProviderName = "memory"
|
||||
// DumpVersion defines the version for the dump.
|
||||
// For restore/load we support the current version and the previous one
|
||||
DumpVersion = 5
|
||||
DumpVersion = 6
|
||||
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
bcryptPwdPrefix = "$2a$"
|
||||
|
@ -73,7 +74,6 @@ const (
|
|||
md5cryptPwdPrefix = "$1$"
|
||||
md5cryptApr1PwdPrefix = "$apr1$"
|
||||
sha512cryptPwdPrefix = "$6$"
|
||||
manageUsersDisabledError = "please set manage_users to 1 in your configuration to enable this method"
|
||||
trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
|
||||
operationAdd = "add"
|
||||
operationUpdate = "update"
|
||||
|
@ -123,9 +123,11 @@ var (
|
|||
sqlTableUsers = "users"
|
||||
sqlTableFolders = "folders"
|
||||
sqlTableFoldersMapping = "folders_mapping"
|
||||
sqlTableAdmins = "admins"
|
||||
sqlTableSchemaVersion = "schema_version"
|
||||
argon2Params *argon2id.Params
|
||||
lastLoginMinDelay = 10 * time.Minute
|
||||
usernameRegex = regexp.MustCompile("^[a-zA-Z0-9-_.~]+$")
|
||||
)
|
||||
|
||||
type schemaVersion struct {
|
||||
|
@ -185,8 +187,6 @@ type Config struct {
|
|||
ConnectionString string `json:"connection_string" mapstructure:"connection_string"`
|
||||
// prefix for SQL tables
|
||||
SQLTablesPrefix string `json:"sql_tables_prefix" mapstructure:"sql_tables_prefix"`
|
||||
// Set to 0 to disable users management, 1 to enable
|
||||
ManageUsers int `json:"manage_users" mapstructure:"manage_users"`
|
||||
// Set the preferred way to track users quota between the following choices:
|
||||
// 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
|
||||
// 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
|
||||
|
@ -277,6 +277,7 @@ type Config struct {
|
|||
type BackupData struct {
|
||||
Users []User `json:"users"`
|
||||
Folders []vfs.BaseVirtualFolder `json:"folders"`
|
||||
Admins []Admin `json:"admins"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
|
@ -334,6 +335,13 @@ func (e *ValidationError) Error() string {
|
|||
return fmt.Sprintf("Validation error: %s", e.err)
|
||||
}
|
||||
|
||||
// NewValidationError returns a validation errors
|
||||
func NewValidationError(error string) *ValidationError {
|
||||
return &ValidationError{
|
||||
err: error,
|
||||
}
|
||||
}
|
||||
|
||||
// MethodDisabledError raised if a method is disabled in config file.
|
||||
// For example, if user management is disabled, this error is raised
|
||||
// every time a user operation is done using the REST API
|
||||
|
@ -370,9 +378,8 @@ type Provider interface {
|
|||
addUser(user *User) error
|
||||
updateUser(user *User) error
|
||||
deleteUser(user *User) error
|
||||
getUsers(limit int, offset int, order string, username string) ([]User, error)
|
||||
getUsers(limit int, offset int, order string) ([]User, error)
|
||||
dumpUsers() ([]User, error)
|
||||
getUserByID(ID int64) (User, error)
|
||||
updateLastLogin(username string) error
|
||||
getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error)
|
||||
getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error)
|
||||
|
@ -381,6 +388,13 @@ type Provider interface {
|
|||
updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error
|
||||
getUsedFolderQuota(mappedPath string) (int, int64, error)
|
||||
dumpFolders() ([]vfs.BaseVirtualFolder, error)
|
||||
adminExists(username string) (Admin, error)
|
||||
addAdmin(admin *Admin) error
|
||||
updateAdmin(admin *Admin) error
|
||||
deleteAdmin(admin *Admin) error
|
||||
getAdmins(limit int, offset int, order string) ([]Admin, error)
|
||||
dumpAdmins() ([]Admin, error)
|
||||
validateAdminAndPass(username, password, ip string) (Admin, error)
|
||||
checkAvailability() error
|
||||
close() error
|
||||
reloadConfig() error
|
||||
|
@ -391,7 +405,7 @@ type Provider interface {
|
|||
|
||||
// Initialize the data provider.
|
||||
// An error is returned if the configured driver is invalid or if the data provider cannot be initialized
|
||||
func Initialize(cnf Config, basePath string) error {
|
||||
func Initialize(cnf Config, basePath string, checkAdmins bool) error {
|
||||
var err error
|
||||
config = cnf
|
||||
|
||||
|
@ -408,6 +422,13 @@ func Initialize(cnf Config, basePath string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argon2Params = &argon2id.Params{
|
||||
Memory: cnf.PasswordHashing.Argon2Options.Memory,
|
||||
Iterations: cnf.PasswordHashing.Argon2Options.Iterations,
|
||||
Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism,
|
||||
SaltLength: 16,
|
||||
KeyLength: 32,
|
||||
}
|
||||
if cnf.UpdateMode == 0 {
|
||||
err = provider.initializeDatabase()
|
||||
if err != nil && err != ErrNoInitRequired {
|
||||
|
@ -423,16 +444,16 @@ func Initialize(cnf Config, basePath string) error {
|
|||
providerLog(logger.LevelWarn, "database migration error: %v", err)
|
||||
return err
|
||||
}
|
||||
if checkAdmins {
|
||||
err = checkDefaultAdmin()
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "check default admin error: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
providerLog(logger.LevelInfo, "database initialization/migration skipped, manual mode is configured")
|
||||
}
|
||||
argon2Params = &argon2id.Params{
|
||||
Memory: cnf.PasswordHashing.Argon2Options.Memory,
|
||||
Iterations: cnf.PasswordHashing.Argon2Options.Iterations,
|
||||
Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism,
|
||||
SaltLength: 16,
|
||||
KeyLength: 32,
|
||||
}
|
||||
startAvailabilityTimer()
|
||||
return nil
|
||||
}
|
||||
|
@ -476,13 +497,29 @@ func validateSQLTablesPrefix() error {
|
|||
sqlTableUsers = config.SQLTablesPrefix + sqlTableUsers
|
||||
sqlTableFolders = config.SQLTablesPrefix + sqlTableFolders
|
||||
sqlTableFoldersMapping = config.SQLTablesPrefix + sqlTableFoldersMapping
|
||||
sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins
|
||||
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
|
||||
providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v schema version %#v",
|
||||
sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableSchemaVersion)
|
||||
providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v admins %#v schema version %#v",
|
||||
sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableAdmins, sqlTableSchemaVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDefaultAdmin() error {
|
||||
admins, err := provider.getAdmins(1, 0, OrderASC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(admins) > 0 {
|
||||
return nil
|
||||
}
|
||||
logger.Debug(logSender, "", "no admins found, try to create the default one")
|
||||
// we need to create the default admin
|
||||
admin := &Admin{}
|
||||
admin.setDefaults()
|
||||
return provider.addAdmin(admin)
|
||||
}
|
||||
|
||||
// InitializeDatabase creates the initial database structure
|
||||
func InitializeDatabase(cnf Config, basePath string) error {
|
||||
config = cnf
|
||||
|
@ -525,6 +562,11 @@ func RevertDatabase(cnf Config, basePath string, targetVersion int) error {
|
|||
return provider.revertDatabase(targetVersion)
|
||||
}
|
||||
|
||||
// CheckAdminAndPass validates the given admin and password connecting from ip
|
||||
func CheckAdminAndPass(username, password, ip string) (Admin, error) {
|
||||
return provider.validateAdminAndPass(username, password, ip)
|
||||
}
|
||||
|
||||
// CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
|
||||
func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
|
||||
|
@ -583,9 +625,6 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard
|
|||
|
||||
// UpdateLastLogin updates the last login fields for the given SFTP user
|
||||
func UpdateLastLogin(user User) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
lastLogin := utils.GetTimeFromMsecSinceEpoch(user.LastLogin)
|
||||
diff := -time.Until(lastLogin)
|
||||
if diff < 0 || diff > lastLoginMinDelay {
|
||||
|
@ -606,9 +645,6 @@ func UpdateUserQuota(user User, filesAdd int, sizeAdd int64, reset bool) error {
|
|||
} else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() {
|
||||
return nil
|
||||
}
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
if filesAdd == 0 && sizeAdd == 0 && !reset {
|
||||
return nil
|
||||
}
|
||||
|
@ -621,9 +657,6 @@ func UpdateVirtualFolderQuota(vfolder vfs.BaseVirtualFolder, filesAdd int, sizeA
|
|||
if config.TrackQuota == 0 {
|
||||
return &MethodDisabledError{err: trackQuotaDisabledError}
|
||||
}
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
if filesAdd == 0 && sizeAdd == 0 && !reset {
|
||||
return nil
|
||||
}
|
||||
|
@ -646,17 +679,37 @@ func GetUsedVirtualFolderQuota(mappedPath string) (int, int64, error) {
|
|||
return provider.getUsedFolderQuota(mappedPath)
|
||||
}
|
||||
|
||||
// UserExists checks if the given SFTP username exists, returns an error if no match is found
|
||||
// AddAdmin adds a new SFTPGo admin
|
||||
func AddAdmin(admin *Admin) error {
|
||||
return provider.addAdmin(admin)
|
||||
}
|
||||
|
||||
// UpdateAdmin updates an existing SFTPGo admin
|
||||
func UpdateAdmin(admin *Admin) error {
|
||||
return provider.updateAdmin(admin)
|
||||
}
|
||||
|
||||
// DeleteAdmin deletes an existing SFTPGo admin
|
||||
func DeleteAdmin(username string) error {
|
||||
admin, err := provider.adminExists(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.deleteAdmin(&admin)
|
||||
}
|
||||
|
||||
// AdminExists returns the given admins if it exists
|
||||
func AdminExists(username string) (Admin, error) {
|
||||
return provider.adminExists(username)
|
||||
}
|
||||
|
||||
// UserExists checks if the given SFTPGo username exists, returns an error if no match is found
|
||||
func UserExists(username string) (User, error) {
|
||||
return provider.userExists(username)
|
||||
}
|
||||
|
||||
// AddUser adds a new SFTPGo user.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func AddUser(user *User) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
err := provider.addUser(user)
|
||||
if err == nil {
|
||||
go executeAction(operationAdd, *user)
|
||||
|
@ -665,11 +718,7 @@ func AddUser(user *User) error {
|
|||
}
|
||||
|
||||
// UpdateUser updates an existing SFTPGo user.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func UpdateUser(user *User) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
err := provider.updateUser(user)
|
||||
if err == nil {
|
||||
RemoveCachedWebDAVUser(user.Username)
|
||||
|
@ -679,15 +728,15 @@ func UpdateUser(user *User) error {
|
|||
}
|
||||
|
||||
// DeleteUser deletes an existing SFTPGo user.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func DeleteUser(user *User) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
func DeleteUser(username string) error {
|
||||
user, err := provider.userExists(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := provider.deleteUser(user)
|
||||
err = provider.deleteUser(&user)
|
||||
if err == nil {
|
||||
RemoveCachedWebDAVUser(user.Username)
|
||||
go executeAction(operationDelete, *user)
|
||||
go executeAction(operationDelete, user)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -699,32 +748,28 @@ func ReloadConfig() error {
|
|||
return provider.reloadConfig()
|
||||
}
|
||||
|
||||
// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
|
||||
func GetUsers(limit, offset int, order string, username string) ([]User, error) {
|
||||
return provider.getUsers(limit, offset, order, username)
|
||||
// GetAdmins returns an array of admins respecting limit and offset
|
||||
func GetAdmins(limit, offset int, order string) ([]Admin, error) {
|
||||
return provider.getAdmins(limit, offset, order)
|
||||
}
|
||||
|
||||
// GetUserByID returns the user with the given database ID if a match is found or an error
|
||||
func GetUserByID(ID int64) (User, error) {
|
||||
return provider.getUserByID(ID)
|
||||
// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
|
||||
func GetUsers(limit, offset int, order string) ([]User, error) {
|
||||
return provider.getUsers(limit, offset, order)
|
||||
}
|
||||
|
||||
// AddFolder adds a new virtual folder.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func AddFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
return provider.addFolder(folder)
|
||||
}
|
||||
|
||||
// DeleteFolder deletes an existing folder.
|
||||
// ManageUsers configuration must be set to 1 to enable this method
|
||||
func DeleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
func DeleteFolder(folderPath string) error {
|
||||
folder, err := provider.getFolderByPath(folderPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.deleteFolder(folder)
|
||||
return provider.deleteFolder(&folder)
|
||||
}
|
||||
|
||||
// GetFolderByPath returns the folder with the specified path if any
|
||||
|
@ -740,7 +785,6 @@ func GetFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualF
|
|||
// DumpData returns all users and folders
|
||||
func DumpData() (BackupData, error) {
|
||||
var data BackupData
|
||||
data.Version = DumpVersion
|
||||
users, err := provider.dumpUsers()
|
||||
if err != nil {
|
||||
return data, err
|
||||
|
@ -749,8 +793,14 @@ func DumpData() (BackupData, error) {
|
|||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
admins, err := provider.dumpAdmins()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.Users = users
|
||||
data.Folders = folders
|
||||
data.Admins = admins
|
||||
data.Version = DumpVersion
|
||||
return data, err
|
||||
}
|
||||
|
||||
|
@ -974,7 +1024,7 @@ func validatePermissions(user *User) error {
|
|||
if utils.IsStringInSlice(PermAny, perms) {
|
||||
permissions[cleanedDir] = []string{PermAny}
|
||||
} else {
|
||||
permissions[cleanedDir] = perms
|
||||
permissions[cleanedDir] = utils.RemoveDuplicates(perms)
|
||||
}
|
||||
}
|
||||
user.Permissions = permissions
|
||||
|
@ -1238,6 +1288,9 @@ func validateBaseParams(user *User) error {
|
|||
if user.Username == "" {
|
||||
return &ValidationError{err: "username is mandatory"}
|
||||
}
|
||||
if !usernameRegex.MatchString(user.Username) {
|
||||
return &ValidationError{err: fmt.Sprintf("username %#v is not valid", user.Username)}
|
||||
}
|
||||
if user.HomeDir == "" {
|
||||
return &ValidationError{err: "home_dir is mandatory"}
|
||||
}
|
||||
|
@ -1251,7 +1304,7 @@ func validateBaseParams(user *User) error {
|
|||
}
|
||||
|
||||
func createUserPasswordHash(user *User) error {
|
||||
if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
|
||||
if user.Password != "" && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
|
||||
pwd, err := argon2id.CreateHash(user.Password, argon2Params)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -26,14 +26,16 @@ type memoryProviderHandle struct {
|
|||
isClosed bool
|
||||
// slice with ordered usernames
|
||||
usernames []string
|
||||
// mapping between ID and username
|
||||
usersIdx map[int64]string
|
||||
// map for users, username is the key
|
||||
users map[string]User
|
||||
// map for virtual folders, MappedPath is the key
|
||||
vfolders map[string]vfs.BaseVirtualFolder
|
||||
// slice with ordered folders mapped path
|
||||
vfoldersPaths []string
|
||||
// map for admins, username is the key
|
||||
admins map[string]Admin
|
||||
// slice with ordered admins
|
||||
adminsUsernames []string
|
||||
}
|
||||
|
||||
// MemoryProvider auth provider for a memory store
|
||||
|
@ -50,14 +52,15 @@ func initializeMemoryProvider(basePath string) {
|
|||
configFile = filepath.Join(basePath, configFile)
|
||||
}
|
||||
}
|
||||
provider = MemoryProvider{
|
||||
provider = &MemoryProvider{
|
||||
dbHandle: &memoryProviderHandle{
|
||||
isClosed: false,
|
||||
usernames: []string{},
|
||||
usersIdx: make(map[int64]string),
|
||||
users: make(map[string]User),
|
||||
vfolders: make(map[string]vfs.BaseVirtualFolder),
|
||||
vfoldersPaths: []string{},
|
||||
admins: make(map[string]Admin),
|
||||
adminsUsernames: []string{},
|
||||
configFile: configFile,
|
||||
},
|
||||
}
|
||||
|
@ -67,7 +70,7 @@ func initializeMemoryProvider(basePath string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (p MemoryProvider) checkAvailability() error {
|
||||
func (p *MemoryProvider) checkAvailability() error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -76,7 +79,7 @@ func (p MemoryProvider) checkAvailability() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) close() error {
|
||||
func (p *MemoryProvider) close() error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -86,7 +89,7 @@ func (p MemoryProvider) close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
var user User
|
||||
if password == "" {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
|
@ -99,7 +102,7 @@ func (p MemoryProvider) validateUserAndPass(username, password, ip, protocol str
|
|||
return checkUserAndPass(user, password, ip, protocol)
|
||||
}
|
||||
|
||||
func (p MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
|
||||
func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
|
||||
var user User
|
||||
if len(pubKey) == 0 {
|
||||
return user, "", errors.New("Credentials cannot be null or empty")
|
||||
|
@ -112,19 +115,17 @@ func (p MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (U
|
|||
return checkUserAndPubKey(user, pubKey)
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getUserByID(ID int64) (User, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return User{}, errMemoryProviderClosed
|
||||
func (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
|
||||
admin, err := p.adminExists(username)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
|
||||
return admin, err
|
||||
}
|
||||
if val, ok := p.dbHandle.usersIdx[ID]; ok {
|
||||
return p.userExistsInternal(val)
|
||||
}
|
||||
return User{}, &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)}
|
||||
err = admin.checkUserAndPass(password, ip)
|
||||
return admin, err
|
||||
}
|
||||
|
||||
func (p MemoryProvider) updateLastLogin(username string) error {
|
||||
func (p *MemoryProvider) updateLastLogin(username string) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -139,7 +140,7 @@ func (p MemoryProvider) updateLastLogin(username string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -164,7 +165,7 @@ func (p MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
func (p *MemoryProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -178,7 +179,7 @@ func (p MemoryProvider) getUsedQuota(username string) (int, int64, error) {
|
|||
return user.UsedQuotaFiles, user.UsedQuotaSize, err
|
||||
}
|
||||
|
||||
func (p MemoryProvider) addUser(user *User) error {
|
||||
func (p *MemoryProvider) addUser(user *User) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -199,13 +200,12 @@ func (p MemoryProvider) addUser(user *User) error {
|
|||
user.LastLogin = 0
|
||||
user.VirtualFolders = p.joinVirtualFoldersFields(user)
|
||||
p.dbHandle.users[user.Username] = user.getACopy()
|
||||
p.dbHandle.usersIdx[user.ID] = user.Username
|
||||
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
|
||||
sort.Strings(p.dbHandle.usernames)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) updateUser(user *User) error {
|
||||
func (p *MemoryProvider) updateUser(user *User) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -227,12 +227,13 @@ func (p MemoryProvider) updateUser(user *User) error {
|
|||
user.UsedQuotaSize = u.UsedQuotaSize
|
||||
user.UsedQuotaFiles = u.UsedQuotaFiles
|
||||
user.LastLogin = u.LastLogin
|
||||
user.ID = u.ID
|
||||
// pre-login and external auth hook will use the passed *user so save a copy
|
||||
p.dbHandle.users[user.Username] = user.getACopy()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) deleteUser(user *User) error {
|
||||
func (p *MemoryProvider) deleteUser(user *User) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -246,7 +247,6 @@ func (p MemoryProvider) deleteUser(user *User) error {
|
|||
p.removeUserFromFolderMapping(oldFolder.MappedPath, u.Username)
|
||||
}
|
||||
delete(p.dbHandle.users, user.Username)
|
||||
delete(p.dbHandle.usersIdx, user.ID)
|
||||
// this could be more efficient
|
||||
p.dbHandle.usernames = make([]string, 0, len(p.dbHandle.users))
|
||||
for username := range p.dbHandle.users {
|
||||
|
@ -256,7 +256,7 @@ func (p MemoryProvider) deleteUser(user *User) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) dumpUsers() ([]User, error) {
|
||||
func (p *MemoryProvider) dumpUsers() ([]User, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
users := make([]User, 0, len(p.dbHandle.usernames))
|
||||
|
@ -276,7 +276,7 @@ func (p MemoryProvider) dumpUsers() ([]User, error) {
|
|||
return users, err
|
||||
}
|
||||
|
||||
func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, len(p.dbHandle.vfoldersPaths))
|
||||
|
@ -289,7 +289,7 @@ func (p MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
|||
return folders, nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
||||
users := make([]User, 0, limit)
|
||||
var err error
|
||||
p.dbHandle.Lock()
|
||||
|
@ -300,16 +300,6 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s
|
|||
if limit <= 0 {
|
||||
return users, err
|
||||
}
|
||||
if len(username) > 0 {
|
||||
if offset == 0 {
|
||||
user, err := p.userExistsInternal(username)
|
||||
if err == nil {
|
||||
user.HideConfidentialData()
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
return users, err
|
||||
}
|
||||
itNum := 0
|
||||
if order == OrderASC {
|
||||
for _, username := range p.dbHandle.usernames {
|
||||
|
@ -344,7 +334,7 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s
|
|||
return users, err
|
||||
}
|
||||
|
||||
func (p MemoryProvider) userExists(username string) (User, error) {
|
||||
func (p *MemoryProvider) userExists(username string) (User, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -353,14 +343,152 @@ func (p MemoryProvider) userExists(username string) (User, error) {
|
|||
return p.userExistsInternal(username)
|
||||
}
|
||||
|
||||
func (p MemoryProvider) userExistsInternal(username string) (User, error) {
|
||||
func (p *MemoryProvider) userExistsInternal(username string) (User, error) {
|
||||
if val, ok := p.dbHandle.users[username]; ok {
|
||||
return val.getACopy(), nil
|
||||
}
|
||||
return User{}, &RecordNotFoundError{err: fmt.Sprintf("username %#v does not exist", username)}
|
||||
}
|
||||
|
||||
func (p MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *MemoryProvider) addAdmin(admin *Admin) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
err := admin.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = p.adminExistsInternal(admin.Username)
|
||||
if err == nil {
|
||||
return fmt.Errorf("admin %#v already exists", admin.Username)
|
||||
}
|
||||
admin.ID = p.getNextAdminID()
|
||||
p.dbHandle.admins[admin.Username] = admin.getACopy()
|
||||
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username)
|
||||
sort.Strings(p.dbHandle.adminsUsernames)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) updateAdmin(admin *Admin) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
err := admin.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a, err := p.adminExistsInternal(admin.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
admin.ID = a.ID
|
||||
p.dbHandle.admins[admin.Username] = admin.getACopy()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) deleteAdmin(admin *Admin) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
_, err := p.adminExistsInternal(admin.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(p.dbHandle.admins, admin.Username)
|
||||
// this could be more efficient
|
||||
p.dbHandle.adminsUsernames = make([]string, 0, len(p.dbHandle.admins))
|
||||
for username := range p.dbHandle.admins {
|
||||
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, username)
|
||||
}
|
||||
sort.Strings(p.dbHandle.adminsUsernames)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) adminExists(username string) (Admin, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return Admin{}, errMemoryProviderClosed
|
||||
}
|
||||
return p.adminExistsInternal(username)
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) adminExistsInternal(username string) (Admin, error) {
|
||||
if val, ok := p.dbHandle.admins[username]; ok {
|
||||
return val.getACopy(), nil
|
||||
}
|
||||
return Admin{}, &RecordNotFoundError{err: fmt.Sprintf("admin %#v does not exist", username)}
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) dumpAdmins() ([]Admin, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
||||
admins := make([]Admin, 0, len(p.dbHandle.admins))
|
||||
if p.dbHandle.isClosed {
|
||||
return admins, errMemoryProviderClosed
|
||||
}
|
||||
for _, admin := range p.dbHandle.admins {
|
||||
admins = append(admins, admin)
|
||||
}
|
||||
return admins, nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
|
||||
admins := make([]Admin, 0, limit)
|
||||
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
||||
if p.dbHandle.isClosed {
|
||||
return admins, errMemoryProviderClosed
|
||||
}
|
||||
if limit <= 0 {
|
||||
return admins, nil
|
||||
}
|
||||
itNum := 0
|
||||
if order == OrderASC {
|
||||
for _, username := range p.dbHandle.adminsUsernames {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
a := p.dbHandle.admins[username]
|
||||
admin := a.getACopy()
|
||||
admin.HideConfidentialData()
|
||||
admins = append(admins, admin)
|
||||
if len(admins) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := len(p.dbHandle.adminsUsernames) - 1; i >= 0; i-- {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
username := p.dbHandle.adminsUsernames[i]
|
||||
a := p.dbHandle.admins[username]
|
||||
admin := a.getACopy()
|
||||
admin.HideConfidentialData()
|
||||
admins = append(admins, admin)
|
||||
if len(admins) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return admins, nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -383,7 +511,7 @@ func (p MemoryProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeA
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
func (p *MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -397,7 +525,7 @@ func (p MemoryProvider) getUsedFolderQuota(mappedPath string) (int, int64, error
|
|||
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
|
||||
}
|
||||
|
||||
func (p MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder {
|
||||
func (p *MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder {
|
||||
var folders []vfs.VirtualFolder
|
||||
for _, folder := range user.VirtualFolders {
|
||||
f, err := p.addOrGetFolderInternal(folder.MappedPath, user.Username, folder.UsedQuotaSize, folder.UsedQuotaFiles,
|
||||
|
@ -413,7 +541,7 @@ func (p MemoryProvider) joinVirtualFoldersFields(user *User) []vfs.VirtualFolder
|
|||
return folders
|
||||
}
|
||||
|
||||
func (p MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) {
|
||||
func (p *MemoryProvider) removeUserFromFolderMapping(mappedPath, username string) {
|
||||
folder, err := p.folderExistsInternal(mappedPath)
|
||||
if err == nil {
|
||||
var usernames []string
|
||||
|
@ -427,7 +555,7 @@ func (p MemoryProvider) removeUserFromFolderMapping(mappedPath, username string)
|
|||
}
|
||||
}
|
||||
|
||||
func (p MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) {
|
||||
func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) {
|
||||
p.dbHandle.vfolders[folder.MappedPath] = folder
|
||||
if !utils.IsStringInSlice(folder.MappedPath, p.dbHandle.vfoldersPaths) {
|
||||
p.dbHandle.vfoldersPaths = append(p.dbHandle.vfoldersPaths, folder.MappedPath)
|
||||
|
@ -435,7 +563,7 @@ func (p MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolde
|
|||
}
|
||||
}
|
||||
|
||||
func (p MemoryProvider) addOrGetFolderInternal(mappedPath, username string, usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64) (vfs.BaseVirtualFolder, error) {
|
||||
func (p *MemoryProvider) addOrGetFolderInternal(mappedPath, username string, usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64) (vfs.BaseVirtualFolder, error) {
|
||||
folder, err := p.folderExistsInternal(mappedPath)
|
||||
if _, ok := err.(*RecordNotFoundError); ok {
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
|
@ -456,14 +584,14 @@ func (p MemoryProvider) addOrGetFolderInternal(mappedPath, username string, used
|
|||
return folder, err
|
||||
}
|
||||
|
||||
func (p MemoryProvider) folderExistsInternal(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
func (p *MemoryProvider) folderExistsInternal(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
if val, ok := p.dbHandle.vfolders[mappedPath]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return vfs.BaseVirtualFolder{}, &RecordNotFoundError{err: fmt.Sprintf("folder %#v does not exist", mappedPath)}
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *MemoryProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, limit)
|
||||
var err error
|
||||
p.dbHandle.Lock()
|
||||
|
@ -514,7 +642,7 @@ func (p MemoryProvider) getFolders(limit, offset int, order, folderPath string)
|
|||
return folders, err
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
func (p *MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -523,7 +651,7 @@ func (p MemoryProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolde
|
|||
return p.folderExistsInternal(mappedPath)
|
||||
}
|
||||
|
||||
func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -544,7 +672,7 @@ func (p MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
|
@ -577,17 +705,17 @@ func (p MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getNextID() int64 {
|
||||
func (p *MemoryProvider) getNextID() int64 {
|
||||
nextID := int64(1)
|
||||
for id := range p.dbHandle.usersIdx {
|
||||
if id >= nextID {
|
||||
nextID = id + 1
|
||||
for _, v := range p.dbHandle.users {
|
||||
if v.ID >= nextID {
|
||||
nextID = v.ID + 1
|
||||
}
|
||||
}
|
||||
return nextID
|
||||
}
|
||||
|
||||
func (p MemoryProvider) getNextFolderID() int64 {
|
||||
func (p *MemoryProvider) getNextFolderID() int64 {
|
||||
nextID := int64(1)
|
||||
for _, v := range p.dbHandle.vfolders {
|
||||
if v.ID >= nextID {
|
||||
|
@ -597,17 +725,28 @@ func (p MemoryProvider) getNextFolderID() int64 {
|
|||
return nextID
|
||||
}
|
||||
|
||||
func (p MemoryProvider) clear() {
|
||||
func (p *MemoryProvider) getNextAdminID() int64 {
|
||||
nextID := int64(1)
|
||||
for _, a := range p.dbHandle.admins {
|
||||
if a.ID >= nextID {
|
||||
nextID = a.ID + 1
|
||||
}
|
||||
}
|
||||
return nextID
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) clear() {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
p.dbHandle.usernames = []string{}
|
||||
p.dbHandle.usersIdx = make(map[int64]string)
|
||||
p.dbHandle.users = make(map[string]User)
|
||||
p.dbHandle.vfoldersPaths = []string{}
|
||||
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)
|
||||
p.dbHandle.admins = make(map[string]Admin)
|
||||
p.dbHandle.adminsUsernames = []string{}
|
||||
}
|
||||
|
||||
func (p MemoryProvider) reloadConfig() error {
|
||||
func (p *MemoryProvider) reloadConfig() error {
|
||||
if p.dbHandle.configFile == "" {
|
||||
providerLog(logger.LevelDebug, "no users configuration file defined")
|
||||
return nil
|
||||
|
@ -676,14 +815,14 @@ func (p MemoryProvider) reloadConfig() error {
|
|||
}
|
||||
|
||||
// initializeDatabase does nothing, no initilization is needed for memory provider
|
||||
func (p MemoryProvider) initializeDatabase() error {
|
||||
func (p *MemoryProvider) initializeDatabase() error {
|
||||
return ErrNoInitRequired
|
||||
}
|
||||
|
||||
func (p MemoryProvider) migrateDatabase() error {
|
||||
func (p *MemoryProvider) migrateDatabase() error {
|
||||
return ErrNoInitRequired
|
||||
}
|
||||
|
||||
func (p MemoryProvider) revertDatabase(targetVersion int) error {
|
||||
func (p *MemoryProvider) revertDatabase(targetVersion int) error {
|
||||
return errors.New("memory provider does not store data, revert not possible")
|
||||
}
|
||||
|
|
|
@ -40,6 +40,10 @@ const (
|
|||
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;"
|
||||
mysqlV6SQL = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;"
|
||||
mysqlV6DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `additional_info`;"
|
||||
mysqlV7SQL = "CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
|
||||
"`password` varchar(255) NOT NULL, `email` varchar(255) NULL, `status` integer NOT NULL, `permissions` longtext NOT NULL, " +
|
||||
"`filters` longtext NULL, `additional_info` longtext NULL);"
|
||||
mysqlV7DownSQL = "DROP TABLE `{{admins}}` CASCADE;"
|
||||
)
|
||||
|
||||
// MySQLProvider auth provider for MySQL/MariaDB database
|
||||
|
@ -65,7 +69,7 @@ func initializeMySQLProvider() error {
|
|||
dbHandle.SetMaxIdleConns(2)
|
||||
}
|
||||
dbHandle.SetConnMaxLifetime(240 * time.Second)
|
||||
provider = MySQLProvider{dbHandle: dbHandle}
|
||||
provider = &MySQLProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v",
|
||||
getMySQLConnectionString(true), err)
|
||||
|
@ -87,98 +91,122 @@ func getMySQLConnectionString(redactedPwd bool) string {
|
|||
return connectionString
|
||||
}
|
||||
|
||||
func (p MySQLProvider) checkAvailability() error {
|
||||
func (p *MySQLProvider) checkAvailability() error {
|
||||
return sqlCommonCheckAvailability(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
func (p *MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
|
||||
func (p *MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
|
||||
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getUserByID(ID int64) (User, error) {
|
||||
return sqlCommonGetUserByID(ID, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
func (p *MySQLProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) updateLastLogin(username string) error {
|
||||
func (p *MySQLProvider) updateLastLogin(username string) error {
|
||||
return sqlCommonUpdateLastLogin(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonCheckUserExists(username, p.dbHandle)
|
||||
func (p *MySQLProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonGetUserByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) addUser(user *User) error {
|
||||
func (p *MySQLProvider) addUser(user *User) error {
|
||||
return sqlCommonAddUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) updateUser(user *User) error {
|
||||
func (p *MySQLProvider) updateUser(user *User) error {
|
||||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) deleteUser(user *User) error {
|
||||
func (p *MySQLProvider) deleteUser(user *User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) dumpUsers() ([]User, error) {
|
||||
func (p *MySQLProvider) dumpUsers() ([]User, error) {
|
||||
return sqlCommonDumpUsers(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||
func (p *MySQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonDumpFolders(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *MySQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
func (p *MySQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonAddFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *MySQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonDeleteFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *MySQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
func (p *MySQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p MySQLProvider) close() error {
|
||||
func (p *MySQLProvider) adminExists(username string) (Admin, error) {
|
||||
return sqlCommonGetAdminByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) addAdmin(admin *Admin) error {
|
||||
return sqlCommonAddAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) updateAdmin(admin *Admin) error {
|
||||
return sqlCommonUpdateAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) deleteAdmin(admin *Admin) error {
|
||||
return sqlCommonDeleteAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
|
||||
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) dumpAdmins() ([]Admin, error) {
|
||||
return sqlCommonDumpAdmins(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
|
||||
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p MySQLProvider) reloadConfig() error {
|
||||
func (p *MySQLProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeDatabase creates the initial database structure
|
||||
func (p MySQLProvider) initializeDatabase() error {
|
||||
func (p *MySQLProvider) initializeDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
|
||||
if err == nil && dbVersion.Version > 0 {
|
||||
return ErrNoInitRequired
|
||||
|
@ -206,7 +234,7 @@ func (p MySQLProvider) initializeDatabase() error {
|
|||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (p MySQLProvider) migrateDatabase() error {
|
||||
func (p *MySQLProvider) migrateDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -226,6 +254,8 @@ func (p MySQLProvider) migrateDatabase() error {
|
|||
return updateMySQLDatabaseFromV4(p.dbHandle)
|
||||
case 5:
|
||||
return updateMySQLDatabaseFromV5(p.dbHandle)
|
||||
case 6:
|
||||
return updateMySQLDatabaseFromV6(p.dbHandle)
|
||||
default:
|
||||
if dbVersion.Version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
|
||||
|
@ -238,7 +268,7 @@ func (p MySQLProvider) migrateDatabase() error {
|
|||
}
|
||||
}
|
||||
|
||||
func (p MySQLProvider) revertDatabase(targetVersion int) error {
|
||||
func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -247,6 +277,16 @@ func (p MySQLProvider) revertDatabase(targetVersion int) error {
|
|||
return fmt.Errorf("current version match target version, nothing to do")
|
||||
}
|
||||
switch dbVersion.Version {
|
||||
case 7:
|
||||
err = downgradeMySQLDatabaseFrom7To6(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
|
||||
case 6:
|
||||
err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
|
||||
if err != nil {
|
||||
|
@ -293,7 +333,15 @@ func updateMySQLDatabaseFromV4(dbHandle *sql.DB) error {
|
|||
}
|
||||
|
||||
func updateMySQLDatabaseFromV5(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom5To6(dbHandle)
|
||||
err := updateMySQLDatabaseFrom5To6(dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateMySQLDatabaseFromV6(dbHandle)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFromV6(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom6To7(dbHandle)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom1To2(dbHandle *sql.DB) error {
|
||||
|
@ -325,6 +373,20 @@ func updateMySQLDatabaseFrom5To6(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom6To7(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 6 -> 7")
|
||||
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
|
||||
sql := strings.Replace(mysqlV7SQL, "{{admins}}", sqlTableAdmins, 1)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom7To6(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 7 -> 6")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
|
||||
sql := strings.Replace(mysqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom6To5(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 6 -> 5")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
|
||||
|
|
|
@ -40,6 +40,11 @@ CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
|||
`
|
||||
pgsqlV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
|
||||
pgsqlV6DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "additional_info" CASCADE;`
|
||||
pgsqlV7SQL = `CREATE TABLE "{{admins}}" ("id" serial NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE,
|
||||
"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL,
|
||||
"filters" text NULL, "additional_info" text NULL);
|
||||
`
|
||||
pgsqlV7DownSQL = `DROP TABLE "{{admins}}" CASCADE;`
|
||||
)
|
||||
|
||||
// PGSQLProvider auth provider for PostgreSQL database
|
||||
|
@ -65,7 +70,7 @@ func initializePGSQLProvider() error {
|
|||
dbHandle.SetMaxIdleConns(2)
|
||||
}
|
||||
dbHandle.SetConnMaxLifetime(240 * time.Second)
|
||||
provider = PGSQLProvider{dbHandle: dbHandle}
|
||||
provider = &PGSQLProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v",
|
||||
getPGSQLConnectionString(true), err)
|
||||
|
@ -88,98 +93,122 @@ func getPGSQLConnectionString(redactedPwd bool) string {
|
|||
return connectionString
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) checkAvailability() error {
|
||||
func (p *PGSQLProvider) checkAvailability() error {
|
||||
return sqlCommonCheckAvailability(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
func (p *PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
|
||||
func (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
|
||||
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getUserByID(ID int64) (User, error) {
|
||||
return sqlCommonGetUserByID(ID, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) updateLastLogin(username string) error {
|
||||
func (p *PGSQLProvider) updateLastLogin(username string) error {
|
||||
return sqlCommonUpdateLastLogin(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonCheckUserExists(username, p.dbHandle)
|
||||
func (p *PGSQLProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonGetUserByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) addUser(user *User) error {
|
||||
func (p *PGSQLProvider) addUser(user *User) error {
|
||||
return sqlCommonAddUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) updateUser(user *User) error {
|
||||
func (p *PGSQLProvider) updateUser(user *User) error {
|
||||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) deleteUser(user *User) error {
|
||||
func (p *PGSQLProvider) deleteUser(user *User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) dumpUsers() ([]User, error) {
|
||||
func (p *PGSQLProvider) dumpUsers() ([]User, error) {
|
||||
return sqlCommonDumpUsers(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||
func (p *PGSQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonDumpFolders(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *PGSQLProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
func (p *PGSQLProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonAddFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *PGSQLProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonDeleteFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *PGSQLProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
func (p *PGSQLProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) close() error {
|
||||
func (p *PGSQLProvider) adminExists(username string) (Admin, error) {
|
||||
return sqlCommonGetAdminByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) addAdmin(admin *Admin) error {
|
||||
return sqlCommonAddAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) updateAdmin(admin *Admin) error {
|
||||
return sqlCommonUpdateAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) deleteAdmin(admin *Admin) error {
|
||||
return sqlCommonDeleteAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
|
||||
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) dumpAdmins() ([]Admin, error) {
|
||||
return sqlCommonDumpAdmins(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
|
||||
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) reloadConfig() error {
|
||||
func (p *PGSQLProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeDatabase creates the initial database structure
|
||||
func (p PGSQLProvider) initializeDatabase() error {
|
||||
func (p *PGSQLProvider) initializeDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
|
||||
if err == nil && dbVersion.Version > 0 {
|
||||
return ErrNoInitRequired
|
||||
|
@ -207,7 +236,7 @@ func (p PGSQLProvider) initializeDatabase() error {
|
|||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) migrateDatabase() error {
|
||||
func (p *PGSQLProvider) migrateDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -227,6 +256,8 @@ func (p PGSQLProvider) migrateDatabase() error {
|
|||
return updatePGSQLDatabaseFromV4(p.dbHandle)
|
||||
case 5:
|
||||
return updatePGSQLDatabaseFromV5(p.dbHandle)
|
||||
case 6:
|
||||
return updatePGSQLDatabaseFromV6(p.dbHandle)
|
||||
default:
|
||||
if dbVersion.Version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
|
||||
|
@ -239,7 +270,7 @@ func (p PGSQLProvider) migrateDatabase() error {
|
|||
}
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) revertDatabase(targetVersion int) error {
|
||||
func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -248,6 +279,16 @@ func (p PGSQLProvider) revertDatabase(targetVersion int) error {
|
|||
return fmt.Errorf("current version match target version, nothing to do")
|
||||
}
|
||||
switch dbVersion.Version {
|
||||
case 7:
|
||||
err = downgradePGSQLDatabaseFrom7To6(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
|
||||
case 6:
|
||||
err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
|
||||
if err != nil {
|
||||
|
@ -294,7 +335,15 @@ func updatePGSQLDatabaseFromV4(dbHandle *sql.DB) error {
|
|||
}
|
||||
|
||||
func updatePGSQLDatabaseFromV5(dbHandle *sql.DB) error {
|
||||
return updatePGSQLDatabaseFrom5To6(dbHandle)
|
||||
err := updatePGSQLDatabaseFrom5To6(dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updatePGSQLDatabaseFromV6(dbHandle)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFromV6(dbHandle *sql.DB) error {
|
||||
return updatePGSQLDatabaseFrom6To7(dbHandle)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFrom1To2(dbHandle *sql.DB) error {
|
||||
|
@ -326,6 +375,20 @@ func updatePGSQLDatabaseFrom5To6(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFrom6To7(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 6 -> 7")
|
||||
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
|
||||
sql := strings.Replace(pgsqlV7SQL, "{{admins}}", sqlTableAdmins, 1)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom7To6(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 7 -> 6")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
|
||||
sql := strings.Replace(pgsqlV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom6To5(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 6 -> 5")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sqlDatabaseVersion = 6
|
||||
sqlDatabaseVersion = 7
|
||||
initialDBVersionSQL = "INSERT INTO {{schema_version}} (version) VALUES (1);"
|
||||
defaultSQLQueryTimeout = 10 * time.Second
|
||||
longSQLQueryTimeout = 60 * time.Second
|
||||
|
@ -26,7 +26,174 @@ type sqlQuerier interface {
|
|||
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
||||
}
|
||||
|
||||
func getUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
|
||||
type sqlScanner interface {
|
||||
Scan(dest ...interface{}) error
|
||||
}
|
||||
|
||||
func sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, error) {
|
||||
var admin Admin
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getAdminByUsernameQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return admin, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
row := stmt.QueryRowContext(ctx, username)
|
||||
|
||||
return getAdminFromDbRow(row)
|
||||
}
|
||||
|
||||
func sqlCommonValidateAdminAndPass(username, password, ip string, dbHandle *sql.DB) (Admin, error) {
|
||||
admin, err := sqlCommonGetAdminByUsername(username, dbHandle)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error authenticating admin %#v: %v", username, err)
|
||||
return admin, err
|
||||
}
|
||||
err = admin.checkUserAndPass(password, ip)
|
||||
return admin, err
|
||||
}
|
||||
|
||||
func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
|
||||
err := admin.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getAddAdminQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
perms, err := json.Marshal(admin.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filters, err := json.Marshal(admin.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, admin.Username, admin.Password, admin.Status, admin.Email, string(perms),
|
||||
string(filters), admin.AdditionalInfo)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
|
||||
err := admin.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getUpdateAdminQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
perms, err := json.Marshal(admin.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filters, err := json.Marshal(admin.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, admin.Password, admin.Status, admin.Email, string(perms), string(filters),
|
||||
admin.AdditionalInfo, admin.Username)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonDeleteAdmin(admin *Admin, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getDeleteAdminQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, admin.Username)
|
||||
return err
|
||||
}
|
||||
|
||||
func sqlCommonGetAdmins(limit, offset int, order string, dbHandle sqlQuerier) ([]Admin, error) {
|
||||
admins := make([]Admin, 0, limit)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getAdminsQuery(order)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.QueryContext(ctx, limit, offset)
|
||||
if err != nil {
|
||||
return admins, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
a, err := getAdminFromDbRow(rows)
|
||||
if err != nil {
|
||||
return admins, err
|
||||
}
|
||||
a.HideConfidentialData()
|
||||
admins = append(admins, a)
|
||||
}
|
||||
|
||||
return admins, rows.Err()
|
||||
}
|
||||
|
||||
func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
|
||||
admins := make([]Admin, 0, 30)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getDumpAdminsQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.QueryContext(ctx)
|
||||
if err != nil {
|
||||
return admins, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
a, err := getAdminFromDbRow(rows)
|
||||
if err != nil {
|
||||
return admins, err
|
||||
}
|
||||
admins = append(admins, a)
|
||||
}
|
||||
|
||||
return admins, rows.Err()
|
||||
}
|
||||
|
||||
func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
|
||||
var user User
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
@ -39,7 +206,7 @@ func getUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
|
|||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRowContext(ctx, username)
|
||||
user, err = getUserFromDbRow(row, nil)
|
||||
user, err = getUserFromDbRow(row)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
@ -51,7 +218,7 @@ func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHan
|
|||
if password == "" {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := getUserByUsername(username, dbHandle)
|
||||
user, err := sqlCommonGetUserByUsername(username, dbHandle)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||
return user, err
|
||||
|
@ -64,7 +231,7 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, dbHandle *sq
|
|||
if len(pubKey) == 0 {
|
||||
return user, "", errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := getUserByUsername(username, dbHandle)
|
||||
user, err := sqlCommonGetUserByUsername(username, dbHandle)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||
return user, "", err
|
||||
|
@ -78,26 +245,6 @@ func sqlCommonCheckAvailability(dbHandle *sql.DB) error {
|
|||
return dbHandle.PingContext(ctx)
|
||||
}
|
||||
|
||||
func sqlCommonGetUserByID(ID int64, dbHandle *sql.DB) (User, error) {
|
||||
var user User
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getUserByIDQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return user, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRowContext(ctx, ID)
|
||||
user, err = getUserFromDbRow(row, nil)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return getUserWithVirtualFolders(user, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
@ -158,25 +305,6 @@ func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func sqlCommonCheckUserExists(username string, dbHandle *sql.DB) (User, error) {
|
||||
var user User
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getUserByUsernameQuery()
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return user, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
row := stmt.QueryRowContext(ctx, username)
|
||||
user, err = getUserFromDbRow(row, nil)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return getUserWithVirtualFolders(user, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
||||
err := validateUser(user)
|
||||
if err != nil {
|
||||
|
@ -317,7 +445,7 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {
|
|||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
u, err := getUserFromDbRow(nil, rows)
|
||||
u, err := getUserFromDbRow(rows)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
@ -327,30 +455,30 @@ func sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {
|
|||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
return getUsersWithVirtualFolders(users, dbHandle)
|
||||
}
|
||||
|
||||
func sqlCommonGetUsers(limit int, offset int, order string, username string, dbHandle sqlQuerier) ([]User, error) {
|
||||
func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier) ([]User, error) {
|
||||
users := make([]User, 0, limit)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
q := getUsersQuery(order, username)
|
||||
q := getUsersQuery(order)
|
||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
var rows *sql.Rows
|
||||
if len(username) > 0 {
|
||||
rows, err = stmt.QueryContext(ctx, username, limit, offset) //nolint:rowserrcheck // rows.Err() is checked
|
||||
} else {
|
||||
rows, err = stmt.QueryContext(ctx, limit, offset) //nolint:rowserrcheck // rows.Err() is checked
|
||||
}
|
||||
|
||||
rows, err := stmt.QueryContext(ctx, limit, offset)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
u, err := getUserFromDbRow(nil, rows)
|
||||
u, err := getUserFromDbRow(rows)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
@ -384,7 +512,47 @@ func updateUserPermissionsFromDb(user *User, permissions string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
|
||||
func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
||||
var admin Admin
|
||||
var email, filters, additionalInfo, permissions sql.NullString
|
||||
|
||||
err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,
|
||||
&filters, &additionalInfo)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return admin, &RecordNotFoundError{err: err.Error()}
|
||||
}
|
||||
return admin, err
|
||||
}
|
||||
|
||||
if permissions.Valid {
|
||||
var perms []string
|
||||
err = json.Unmarshal([]byte(permissions.String), &perms)
|
||||
if err != nil {
|
||||
return admin, err
|
||||
}
|
||||
admin.Permissions = perms
|
||||
}
|
||||
|
||||
if email.Valid {
|
||||
admin.Email = email.String
|
||||
}
|
||||
if filters.Valid {
|
||||
var adminFilters AdminFilters
|
||||
err = json.Unmarshal([]byte(filters.String), &adminFilters)
|
||||
if err == nil {
|
||||
admin.Filters = adminFilters
|
||||
}
|
||||
}
|
||||
if additionalInfo.Valid {
|
||||
admin.AdditionalInfo = additionalInfo.String
|
||||
}
|
||||
|
||||
return admin, err
|
||||
}
|
||||
|
||||
func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||
var user User
|
||||
var permissions sql.NullString
|
||||
var password sql.NullString
|
||||
|
@ -392,18 +560,11 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
|
|||
var filters sql.NullString
|
||||
var fsConfig sql.NullString
|
||||
var additionalInfo sql.NullString
|
||||
var err error
|
||||
if row != nil {
|
||||
err = row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
||||
|
||||
err := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
||||
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
||||
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
|
||||
&additionalInfo)
|
||||
} else {
|
||||
err = rows.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
||||
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
||||
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
|
||||
&additionalInfo)
|
||||
}
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return user, &RecordNotFoundError{err: err.Error()}
|
||||
|
|
|
@ -78,6 +78,10 @@ INSERT INTO "new__users" ("id", "username", "password", "public_keys", "home_dir
|
|||
DROP TABLE "{{users}}";
|
||||
ALTER TABLE "new__users" RENAME TO "{{users}}";
|
||||
`
|
||||
sqliteV7SQL = `CREATE TABLE "{{admins}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE,
|
||||
"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL, "filters" text NULL,
|
||||
"additional_info" text NULL);`
|
||||
sqliteV7DownSQL = `DROP TABLE "{{admins}}";`
|
||||
)
|
||||
|
||||
// SQLiteProvider auth provider for SQLite database
|
||||
|
@ -109,7 +113,7 @@ func initializeSQLiteProvider(basePath string) error {
|
|||
if err == nil {
|
||||
providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %#v", connectionString)
|
||||
dbHandle.SetMaxOpenConns(1)
|
||||
provider = SQLiteProvider{dbHandle: dbHandle}
|
||||
provider = &SQLiteProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
providerLog(logger.LevelWarn, "error creating sqlite database handler, connection string: %#v, error: %v",
|
||||
connectionString, err)
|
||||
|
@ -117,98 +121,122 @@ func initializeSQLiteProvider(basePath string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) checkAvailability() error {
|
||||
func (p *SQLiteProvider) checkAvailability() error {
|
||||
return sqlCommonCheckAvailability(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
func (p *SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||
return sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
|
||||
func (p *SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
|
||||
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getUserByID(ID int64) (User, error) {
|
||||
return sqlCommonGetUserByID(ID, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
func (p *SQLiteProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
return sqlCommonGetUsedQuota(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) updateLastLogin(username string) error {
|
||||
func (p *SQLiteProvider) updateLastLogin(username string) error {
|
||||
return sqlCommonUpdateLastLogin(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonCheckUserExists(username, p.dbHandle)
|
||||
func (p *SQLiteProvider) userExists(username string) (User, error) {
|
||||
return sqlCommonGetUserByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) addUser(user *User) error {
|
||||
func (p *SQLiteProvider) addUser(user *User) error {
|
||||
return sqlCommonAddUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) updateUser(user *User) error {
|
||||
func (p *SQLiteProvider) updateUser(user *User) error {
|
||||
return sqlCommonUpdateUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) deleteUser(user *User) error {
|
||||
func (p *SQLiteProvider) deleteUser(user *User) error {
|
||||
return sqlCommonDeleteUser(user, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) dumpUsers() ([]User, error) {
|
||||
func (p *SQLiteProvider) dumpUsers() ([]User, error) {
|
||||
return sqlCommonDumpUsers(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||
func (p *SQLiteProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
||||
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonDumpFolders(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
func (p *SQLiteProvider) getFolders(limit, offset int, order, folderPath string) ([]vfs.BaseVirtualFolder, error) {
|
||||
return sqlCommonGetFolders(limit, offset, order, folderPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
func (p *SQLiteProvider) getFolderByPath(mappedPath string) (vfs.BaseVirtualFolder, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
return sqlCommonCheckFolderExists(ctx, mappedPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonAddFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
func (p *SQLiteProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
return sqlCommonDeleteFolder(folder, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
func (p *SQLiteProvider) updateFolderQuota(mappedPath string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return sqlCommonUpdateFolderQuota(mappedPath, filesAdd, sizeAdd, reset, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
func (p *SQLiteProvider) getUsedFolderQuota(mappedPath string) (int, int64, error) {
|
||||
return sqlCommonGetFolderUsedQuota(mappedPath, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) close() error {
|
||||
func (p *SQLiteProvider) adminExists(username string) (Admin, error) {
|
||||
return sqlCommonGetAdminByUsername(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) addAdmin(admin *Admin) error {
|
||||
return sqlCommonAddAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) updateAdmin(admin *Admin) error {
|
||||
return sqlCommonUpdateAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) deleteAdmin(admin *Admin) error {
|
||||
return sqlCommonDeleteAdmin(admin, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {
|
||||
return sqlCommonGetAdmins(limit, offset, order, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) dumpAdmins() ([]Admin, error) {
|
||||
return sqlCommonDumpAdmins(p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {
|
||||
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) reloadConfig() error {
|
||||
func (p *SQLiteProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeDatabase creates the initial database structure
|
||||
func (p SQLiteProvider) initializeDatabase() error {
|
||||
func (p *SQLiteProvider) initializeDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)
|
||||
if err == nil && dbVersion.Version > 0 {
|
||||
return ErrNoInitRequired
|
||||
|
@ -236,7 +264,7 @@ func (p SQLiteProvider) initializeDatabase() error {
|
|||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) migrateDatabase() error {
|
||||
func (p *SQLiteProvider) migrateDatabase() error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -256,6 +284,8 @@ func (p SQLiteProvider) migrateDatabase() error {
|
|||
return updateSQLiteDatabaseFromV4(p.dbHandle)
|
||||
case 5:
|
||||
return updateSQLiteDatabaseFromV5(p.dbHandle)
|
||||
case 6:
|
||||
return updateSQLiteDatabaseFromV6(p.dbHandle)
|
||||
default:
|
||||
if dbVersion.Version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
|
||||
|
@ -268,7 +298,7 @@ func (p SQLiteProvider) migrateDatabase() error {
|
|||
}
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) revertDatabase(targetVersion int) error {
|
||||
func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
||||
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -277,6 +307,16 @@ func (p SQLiteProvider) revertDatabase(targetVersion int) error {
|
|||
return fmt.Errorf("current version match target version, nothing to do")
|
||||
}
|
||||
switch dbVersion.Version {
|
||||
case 7:
|
||||
err = downgradeSQLiteDatabaseFrom7To6(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
|
||||
case 6:
|
||||
err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
|
||||
if err != nil {
|
||||
|
@ -323,7 +363,15 @@ func updateSQLiteDatabaseFromV4(dbHandle *sql.DB) error {
|
|||
}
|
||||
|
||||
func updateSQLiteDatabaseFromV5(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom5To6(dbHandle)
|
||||
err := updateSQLiteDatabaseFrom5To6(dbHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateSQLiteDatabaseFromV6(dbHandle)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFromV6(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom6To7(dbHandle)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom1To2(dbHandle *sql.DB) error {
|
||||
|
@ -355,6 +403,20 @@ func updateSQLiteDatabaseFrom5To6(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom6To7(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 6 -> 7")
|
||||
providerLog(logger.LevelInfo, "updating database version: 6 -> 7")
|
||||
sql := strings.Replace(sqliteV7SQL, "{{admins}}", sqlTableAdmins, 1)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 7)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom7To6(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 7 -> 6")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 7 -> 6")
|
||||
sql := strings.Replace(sqliteV7DownSQL, "{{admins}}", sqlTableAdmins, 1)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom6To5(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 6 -> 5")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
|
||||
|
|
|
@ -12,6 +12,7 @@ const (
|
|||
selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," +
|
||||
"used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem,additional_info"
|
||||
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update"
|
||||
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info"
|
||||
)
|
||||
|
||||
func getSQLPlaceholders() []string {
|
||||
|
@ -26,19 +27,40 @@ func getSQLPlaceholders() []string {
|
|||
return placeholders
|
||||
}
|
||||
|
||||
func getAdminByUsernameQuery() string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectAdminFields, sqlTableAdmins, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getAdminsQuery(order string) string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectAdminFields, sqlTableAdmins,
|
||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getDumpAdminsQuery() string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v`, selectAdminFields, sqlTableAdmins)
|
||||
}
|
||||
|
||||
func getAddAdminQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info)
|
||||
VALUES (%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
|
||||
}
|
||||
|
||||
func getUpdateAdminQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v
|
||||
WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
|
||||
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
|
||||
}
|
||||
|
||||
func getDeleteAdminQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %v WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getUserByUsernameQuery() string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getUserByIDQuery() string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE id = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getUsersQuery(order string, username string) string {
|
||||
if len(username) > 0 {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v ORDER BY username %v LIMIT %v OFFSET %v`,
|
||||
selectUserFields, sqlTableUsers, sqlPlaceholders[0], order, sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
}
|
||||
func getUsersQuery(order string) string {
|
||||
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY username %v LIMIT %v OFFSET %v`, selectUserFields, sqlTableUsers,
|
||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
||||
// Available permissions for SFTP users
|
||||
// Available permissions for SFTPGo users
|
||||
const (
|
||||
// All permissions are granted
|
||||
PermAny = "*"
|
||||
|
@ -802,26 +802,12 @@ func (u *User) GetExpirationDateAsString() string {
|
|||
|
||||
// GetAllowedIPAsString returns the allowed IP as comma separated string
|
||||
func (u User) GetAllowedIPAsString() string {
|
||||
result := ""
|
||||
for _, IPMask := range u.Filters.AllowedIP {
|
||||
if len(result) > 0 {
|
||||
result += ","
|
||||
}
|
||||
result += IPMask
|
||||
}
|
||||
return result
|
||||
return strings.Join(u.Filters.AllowedIP, ",")
|
||||
}
|
||||
|
||||
// GetDeniedIPAsString returns the denied IP as comma separated string
|
||||
func (u User) GetDeniedIPAsString() string {
|
||||
result := ""
|
||||
for _, IPMask := range u.Filters.DeniedIP {
|
||||
if len(result) > 0 {
|
||||
result += ","
|
||||
}
|
||||
result += IPMask
|
||||
}
|
||||
return result
|
||||
return strings.Join(u.Filters.DeniedIP, ",")
|
||||
}
|
||||
|
||||
// SetEmptySecretsIfNil sets the secrets to empty if nil
|
||||
|
|
|
@ -15,5 +15,5 @@ SFTPGo supports checking passwords stored with bcrypt, pbkdf2, md5crypt and sha5
|
|||
|
||||
If you want to use your existing accounts, you have these options:
|
||||
|
||||
- you can import your users inside SFTPGo. Take a look at [sftpgo_api_cli](../examples/rest-api-cli#convert-users-from-other-stores "SFTPGo API CLI example"), it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
|
||||
- you can import your users inside SFTPGo. Take a look at [convert users](.../examples/convertusers) script, it can convert and import users from Linux system users and Pure-FTPd/ProFTPD virtual users
|
||||
- you can use an external authentication program
|
||||
|
|
|
@ -38,7 +38,7 @@ The `defender` can also load a permanent block list and/or a safe list of ip add
|
|||
- `safelist_file`, defines the path to a file containing a list of ip addresses and/or networks to never ban.
|
||||
- `blocklist_file`, defines the path to a file containing a list of ip addresses and/or networks to always ban.
|
||||
|
||||
These list must be stored as JSON with the following schema:
|
||||
These list must be stored as JSON conforming to the following schema:
|
||||
|
||||
- `addresses`, list of strings. Each string must be a valid IPv4/IPv6 address.
|
||||
- `networks`, list of strings. Each string must be a valid IPv4/IPv6 CIDR address.
|
||||
|
|
|
@ -160,7 +160,6 @@ The configuration file contains the following sections:
|
|||
- `sslmode`, integer. Used for drivers `mysql` and `postgresql`. 0 disable SSL/TLS connections, 1 require ssl, 2 set ssl mode to `verify-ca` for driver `postgresql` and `skip-verify` for driver `mysql`, 3 set ssl mode to `verify-full` for driver `postgresql` and `preferred` for driver `mysql`
|
||||
- `connectionstring`, string. Provide a custom database connection string. If not empty, this connection string will be used instead of building one using the previous parameters. Leave empty for drivers `bolt` and `memory`
|
||||
- `sql_tables_prefix`, string. Prefix for SQL tables
|
||||
- `manage_users`, integer. Set to 0 to disable users management, 1 to enable
|
||||
- `track_quota`, integer. Set the preferred mode to track users quota between the following choices:
|
||||
- 0, disable quota tracking. REST API to scan users home directories/virtual folders and update quota will do nothing
|
||||
- 1, quota is updated each time a user uploads or deletes a file, even if the user has no quota restrictions
|
||||
|
@ -193,7 +192,6 @@ The configuration file contains the following sections:
|
|||
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
|
||||
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
|
||||
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
|
||||
- `auth_user_file`, string. Path to a file used to store usernames and passwords for basic authentication. This can be an absolute path or a path relative to the config dir. We support HTTP basic authentication, and the file format must conform to the one generated using the Apache `htpasswd` tool. The supported password formats are bcrypt (`$2y$` prefix) and md5 crypt (`$apr1$` prefix). If empty, HTTP authentication is disabled.
|
||||
- `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
|
||||
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
||||
- **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server)
|
||||
|
|
|
@ -4,32 +4,40 @@ SFTPGo exposes REST API to manage, backup, and restore users and folders, and to
|
|||
|
||||
If quota tracking is enabled in the configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP/SCP, or if you change `track_quota` from `2` to `1`, you can rescan the users home dir and update the used quota using the REST API.
|
||||
|
||||
REST API can be protected using HTTP basic authentication and exposed via HTTPS. If you need more advanced security features, you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
|
||||
REST API are protected using JSON Web Tokens (JWT) authentication and can be exposed over HTTPS.
|
||||
|
||||
For example, you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way:
|
||||
The default credentials are:
|
||||
|
||||
```shell
|
||||
ProxyPass /api/v1 http://127.0.0.1:8080/api/v1
|
||||
ProxyPassReverse /api/v1 http://127.0.0.1:8080/api/v1
|
||||
- username: `admin`
|
||||
- password: `password`
|
||||
|
||||
You can get a JWT token using the `/api/v2/token` endpoint, you need to authenticate using HTTP Basic authentication and the credentials of an active administrator. Here is a sample response:
|
||||
|
||||
```json
|
||||
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTA4NzU5NDksImp0aSI6ImMwMjAzbGZjZHJwZDRsMGMxanZnIiwibmJmIjoxNjEwODc1MzE5LCJwZXJtaXNzaW9ucyI6WyIqIl0sInN1YiI6ImlHZ010NlZNU3AzN2tld3hMR3lUV1l2b2p1a2ttSjBodXlJZHBzSWRyOFE9IiwidXNlcm5hbWUiOiJhZG1pbiJ9.dt-UwcWdEMwoGauuiQw8BmgpBAv4YlTaXkyNK-7iRJ4","expires_at":"2021-01-17T09:32:29Z"}
|
||||
```
|
||||
|
||||
and you can add authentication with something like this:
|
||||
once the access token has expired, you need to get a new one.
|
||||
|
||||
```shell
|
||||
<Location /api/v1>
|
||||
AuthType Digest
|
||||
AuthName "Private"
|
||||
AuthDigestDomain "/api/v1"
|
||||
AuthDigestProvider file
|
||||
AuthUserFile "/etc/httpd/conf/auth_digest"
|
||||
Require valid-user
|
||||
</Location>
|
||||
```
|
||||
JWT tokens are not stored and we use a randomly generated secret to sign them so if you restart SFTPGo all the previous tokens will be invalidated and you will get a 401 HTTP response code.
|
||||
|
||||
and, of course, you can configure the web server to use HTTPS.
|
||||
You can create other administrator and assign them the following permissions:
|
||||
|
||||
- add users
|
||||
- edit users
|
||||
- del users
|
||||
- view users
|
||||
- view connections
|
||||
- close connections
|
||||
- view server status
|
||||
- view and start quota scans
|
||||
- view defender
|
||||
- manage defender
|
||||
- manage system
|
||||
- manage admins
|
||||
|
||||
You can also restrict administrator access based on the source IP address. If you are running SFTPGo behind a reverse proxy you need to allow both the proxy IP address and the real client IP.
|
||||
|
||||
The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](../httpd/schema/openapi.yaml "OpenAPI 3 specs").
|
||||
|
||||
A sample CLI client for the REST API can be found inside the source tree [rest-api-cli](../examples/rest-api-cli) directory.
|
||||
|
||||
You can also generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/)
|
||||
You can generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/).
|
||||
|
|
|
@ -61,8 +61,6 @@ sudo systemctl start sftpgo
|
|||
sudo systemctl status sftpgo
|
||||
# automatically start sftpgo on boot
|
||||
sudo systemctl enable sftpgo
|
||||
# optional, install the REST API CLI. It requires python-requests to run
|
||||
sudo install -Dm755 examples/rest-api-cli/sftpgo_api_cli /usr/bin/sftpgo_api_cli
|
||||
# optional, create shell completion script, for example for bash
|
||||
sudo sh -c '/usr/bin/sftpgo gen completion bash > /usr/share/bash-completion/completions/sftpgo'
|
||||
# optional, create man pages
|
||||
|
@ -102,8 +100,6 @@ sudo ln -s /usr/local/opt/sftpgo/init/com.github.drakkan.sftpgo.plist /Library/L
|
|||
sudo launchctl load -w /Library/LaunchDaemons/com.github.drakkan.sftpgo.plist
|
||||
# verify that the service is started
|
||||
sudo launchctl list com.github.drakkan.sftpgo
|
||||
# optional, install the REST API CLI. It requires python-requests to run, this python module is not installed by default
|
||||
sudo cp examples/rest-api-cli/sftpgo_api_cli /usr/local/opt/sftpgo/bin/
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
# Web Admin
|
||||
|
||||
You can easily build your own interface using the exposed REST API. Anyway, SFTPGo also provides a very basic built-in web interface that allows you to manage users and connections.
|
||||
You can easily build your own interface using the exposed [REST API](./rest-api.md). Anyway, SFTPGo also provides a basic built-in web interface that allows you to manage users, virtual folders, admins and connections.
|
||||
With the default `httpd` configuration, the web admin is available at the following URL:
|
||||
|
||||
[http://127.0.0.1:8080/web](http://127.0.0.1:8080/web)
|
||||
|
||||
The web interface can be protected using HTTP basic authentication and exposed via HTTPS. If you need more advanced security features, you can setup a reverse proxy as explained for the [REST API](./rest-api.md).
|
||||
The default credentials are:
|
||||
|
||||
- username: `admin`
|
||||
- password: `password`
|
||||
|
||||
The web interface can be exposed over HTTPS.
|
||||
|
|
49
examples/convertusers/README.md
Normal 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`.
|
208
examples/convertusers/convertusers
Executable 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()
|
|
@ -1,5 +1,7 @@
|
|||
# REST API CLI client
|
||||
|
||||
:warning: This sample client is deprecated and it will work only with api V1 (SFTPGo <= 1.2.2). You can easily build your own client from the OpenAPI schema.
|
||||
|
||||
`sftpgo_api_cli` is a very simple command line client for `SFTPGo` REST API written in python.
|
||||
|
||||
It has the following requirements:
|
||||
|
|
|
@ -14,14 +14,14 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/httpdtest"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
)
|
||||
|
||||
func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
||||
u := getTestUserWithCryptFs()
|
||||
u.QuotaSize = 6553600
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -56,7 +56,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
|||
assert.Len(t, list, 1)
|
||||
assert.Equal(t, testFileSize, int64(list[0].Size))
|
||||
}
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
@ -66,7 +66,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
err = client.Delete(testFileName + "1")
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)
|
||||
|
@ -108,7 +108,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -117,7 +117,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
|||
|
||||
func TestZeroBytesTransfersCryptFs(t *testing.T) {
|
||||
u := getTestUserWithCryptFs()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -146,7 +146,7 @@ func TestZeroBytesTransfersCryptFs(t *testing.T) {
|
|||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -154,7 +154,7 @@ func TestZeroBytesTransfersCryptFs(t *testing.T) {
|
|||
|
||||
func TestResumeCryptFs(t *testing.T) {
|
||||
u := getTestUserWithCryptFs()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -207,7 +207,7 @@ func TestResumeCryptFs(t *testing.T) {
|
|||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/ftpd"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/httpdtest"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
|
@ -132,7 +132,7 @@ func TestMain(m *testing.M) {
|
|||
logger.WarnToConsole("error initializing common: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing data provider: %v", err)
|
||||
os.Exit(1)
|
||||
|
@ -150,7 +150,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
httpdConf.BindPort = 8079
|
||||
httpd.SetBaseURLAndCredentials("http://127.0.0.1:8079", "", "")
|
||||
httpdtest.SetBaseURL("http://127.0.0.1:8079")
|
||||
|
||||
ftpdConf := config.GetFTPDConfig()
|
||||
ftpdConf.Bindings = []ftpd.Binding{
|
||||
|
@ -298,11 +298,11 @@ func TestInitializationFailure(t *testing.T) {
|
|||
func TestBasicFTPHandling(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.QuotaSize = 6553600
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.QuotaSize = 6553600
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
|
@ -332,7 +332,7 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
@ -342,7 +342,7 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
err = client.Delete(testFileName + "1")
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
||||
|
@ -385,9 +385,9 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -396,12 +396,12 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||
|
||||
func TestLoginInvalidPwd(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
user.Password = "wrong"
|
||||
_, err = getFTPClient(user, false)
|
||||
assert.Error(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -425,7 +425,7 @@ func TestLoginExternalAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
providerConf.ExternalAuthHook = extAuthPath
|
||||
providerConf.ExternalAuthScope = 0
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(u, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -441,22 +441,20 @@ func TestLoginExternalAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, users, 1) {
|
||||
user := users[0]
|
||||
assert.Equal(t, defaultUsername, user.Username)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
err = dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf = config.GetProviderConf()
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(extAuthPath)
|
||||
assert.NoError(t, err)
|
||||
|
@ -475,11 +473,10 @@ func TestPreLoginHook(t *testing.T) {
|
|||
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
providerConf.PreLoginHook = preLoginPath
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||
_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(users))
|
||||
client, err := getFTPClient(u, false)
|
||||
if assert.NoError(t, err) {
|
||||
err = checkBasicFTP(client)
|
||||
|
@ -488,10 +485,8 @@ func TestPreLoginHook(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
users, _, err = httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
user := users[0]
|
||||
|
||||
// test login with an existing user
|
||||
client, err = getFTPClient(user, true)
|
||||
|
@ -518,7 +513,7 @@ func TestPreLoginHook(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -527,7 +522,7 @@ func TestPreLoginHook(t *testing.T) {
|
|||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf = config.GetProviderConf()
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(preLoginPath)
|
||||
assert.NoError(t, err)
|
||||
|
@ -540,7 +535,7 @@ func TestPostConnectHook(t *testing.T) {
|
|||
common.Config.PostConnectHook = postConnectPath
|
||||
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
err = ioutil.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
@ -559,7 +554,7 @@ func TestPostConnectHook(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
common.Config.PostConnectHook = "http://127.0.0.1:8079/api/v1/version"
|
||||
common.Config.PostConnectHook = "http://127.0.0.1:8079/healthz"
|
||||
|
||||
client, err = getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -577,7 +572,7 @@ func TestPostConnectHook(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -589,7 +584,7 @@ func TestMaxConnections(t *testing.T) {
|
|||
oldValue := common.Config.MaxTotalConnections
|
||||
common.Config.MaxTotalConnections = 1
|
||||
|
||||
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -600,7 +595,7 @@ func TestMaxConnections(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -618,7 +613,7 @@ func TestDefender(t *testing.T) {
|
|||
err := common.Initialize(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -640,7 +635,7 @@ func TestDefender(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "Access denied, banned client IP")
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -652,7 +647,7 @@ func TestDefender(t *testing.T) {
|
|||
func TestMaxSessions(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.MaxSessions = 1
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -663,7 +658,7 @@ func TestMaxSessions(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -671,7 +666,7 @@ func TestMaxSessions(t *testing.T) {
|
|||
|
||||
func TestZeroBytesTransfers(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, useTLS := range []bool{true, false} {
|
||||
client, err := getFTPClient(user, useTLS)
|
||||
|
@ -699,7 +694,7 @@ func TestZeroBytesTransfers(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -727,7 +722,7 @@ func TestDownloadErrors(t *testing.T) {
|
|||
DeniedPatterns: []string{"*.jpg"},
|
||||
},
|
||||
}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -758,7 +753,7 @@ func TestDownloadErrors(t *testing.T) {
|
|||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -779,7 +774,7 @@ func TestUploadErrors(t *testing.T) {
|
|||
DeniedExtensions: []string{".zip"},
|
||||
},
|
||||
}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -823,7 +818,7 @@ func TestUploadErrors(t *testing.T) {
|
|||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -831,9 +826,9 @@ func TestUploadErrors(t *testing.T) {
|
|||
|
||||
func TestResume(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
client, err := getFTPClient(user, true)
|
||||
|
@ -888,9 +883,9 @@ func TestResume(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -900,12 +895,12 @@ func TestResume(t *testing.T) {
|
|||
func TestDeniedLoginMethod(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
_, err = getFTPClient(user, false)
|
||||
assert.Error(t, err)
|
||||
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndPassword}
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -913,7 +908,7 @@ func TestDeniedLoginMethod(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -923,12 +918,12 @@ func TestDeniedLoginMethod(t *testing.T) {
|
|||
func TestDeniedProtocols(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Filters.DeniedProtocols = []string{common.ProtocolFTP}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
_, err = getFTPClient(user, false)
|
||||
assert.Error(t, err)
|
||||
user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolWebDAV}
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -936,7 +931,7 @@ func TestDeniedProtocols(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -945,11 +940,11 @@ func TestDeniedProtocols(t *testing.T) {
|
|||
func TestQuotaLimits(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.QuotaFiles = 1
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.QuotaFiles = 1
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
testFileSize := int64(65535)
|
||||
|
@ -981,7 +976,7 @@ func TestQuotaLimits(t *testing.T) {
|
|||
// test quota size
|
||||
user.QuotaSize = testFileSize - 1
|
||||
user.QuotaFiles = 0
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -995,7 +990,7 @@ func TestQuotaLimits(t *testing.T) {
|
|||
// now test quota limits while uploading the current file, we have 1 bytes remaining
|
||||
user.QuotaSize = testFileSize + 1
|
||||
user.QuotaFiles = 0
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1031,13 +1026,13 @@ func TestQuotaLimits(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
user.QuotaFiles = 0
|
||||
user.QuotaSize = 0
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1047,11 +1042,11 @@ func TestUploadMaxSize(t *testing.T) {
|
|||
testFileSize := int64(65535)
|
||||
u := getTestUser()
|
||||
u.Filters.MaxUploadFileSize = testFileSize + 1
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.Filters.MaxUploadFileSize = testFileSize + 1
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
|
@ -1084,13 +1079,13 @@ func TestUploadMaxSize(t *testing.T) {
|
|||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
user.Filters.MaxUploadFileSize = 65536000
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1100,7 +1095,7 @@ func TestLoginWithIPilters(t *testing.T) {
|
|||
u := getTestUser()
|
||||
u.Filters.DeniedIP = []string{"192.167.0.0/24", "172.18.0.0/16"}
|
||||
u.Filters.AllowedIP = []string{"172.19.0.0/16"}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if !assert.Error(t, err) {
|
||||
|
@ -1108,7 +1103,7 @@ func TestLoginWithIPilters(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1129,7 +1124,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
|
||||
assert.NoError(t, dataprovider.Close())
|
||||
|
||||
err := dataprovider.Initialize(providerConf, configDir)
|
||||
err := dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if _, err = os.Stat(credentialsFile); err == nil {
|
||||
|
@ -1137,7 +1132,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
assert.NoError(t, os.Remove(credentialsFile))
|
||||
}
|
||||
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
|
@ -1152,7 +1147,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1160,7 +1155,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
assert.NoError(t, dataprovider.Close())
|
||||
assert.NoError(t, config.LoadConfig(configDir, ""))
|
||||
providerConf = config.GetProviderConf()
|
||||
assert.NoError(t, dataprovider.Initialize(providerConf, configDir))
|
||||
assert.NoError(t, dataprovider.Initialize(providerConf, configDir, true))
|
||||
}
|
||||
|
||||
func TestLoginInvalidFs(t *testing.T) {
|
||||
|
@ -1168,7 +1163,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "test"
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
providerConf := config.GetProviderConf()
|
||||
|
@ -1186,7 +1181,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1194,7 +1189,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
|
||||
func TestClientClose(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1207,7 +1202,7 @@ func TestClientClose(t *testing.T) {
|
|||
1*time.Second, 50*time.Millisecond)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1215,9 +1210,9 @@ func TestClientClose(t *testing.T) {
|
|||
|
||||
func TestRename(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
testDir := "adir"
|
||||
|
@ -1262,7 +1257,7 @@ func TestRename(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
user.Permissions[path.Join("/", testDir)] = []string{dataprovider.PermListItems}
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1277,15 +1272,15 @@ func TestRename(t *testing.T) {
|
|||
if user.Username == defaultUsername {
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = allPerms
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1293,9 +1288,9 @@ func TestRename(t *testing.T) {
|
|||
|
||||
func TestSymlink(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
|
@ -1342,9 +1337,9 @@ func TestSymlink(t *testing.T) {
|
|||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1353,9 +1348,9 @@ func TestSymlink(t *testing.T) {
|
|||
func TestStat(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Permissions["/subdir"] = []string{dataprovider.PermUpload}
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
|
@ -1391,9 +1386,9 @@ func TestStat(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1413,7 +1408,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
})
|
||||
err := os.MkdirAll(mappedPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1423,7 +1418,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
|
@ -1432,7 +1427,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
}
|
||||
err = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
folder, _, err = httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
folder, _, err = httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
|
@ -1444,9 +1439,9 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
|
||||
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1466,7 +1461,7 @@ func TestAllocateAvailable(t *testing.T) {
|
|||
})
|
||||
err := os.MkdirAll(mappedPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1488,7 +1483,7 @@ func TestAllocateAvailable(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
user.QuotaSize = 100
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1537,7 +1532,7 @@ func TestAllocateAvailable(t *testing.T) {
|
|||
|
||||
user.Filters.MaxUploadFileSize = 100
|
||||
user.QuotaSize = 0
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1560,7 +1555,7 @@ func TestAllocateAvailable(t *testing.T) {
|
|||
}
|
||||
|
||||
user.QuotaSize = 50
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1572,7 +1567,7 @@ func TestAllocateAvailable(t *testing.T) {
|
|||
|
||||
user.QuotaSize = 1000
|
||||
user.Filters.MaxUploadFileSize = 1
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1582,9 +1577,9 @@ func TestAllocateAvailable(t *testing.T) {
|
|||
assert.Equal(t, "1", response)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
|
||||
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1594,9 +1589,9 @@ func TestAllocateAvailable(t *testing.T) {
|
|||
|
||||
func TestAvailableUnsupportedFs(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(sftpUser, false)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1608,9 +1603,9 @@ func TestAvailableUnsupportedFs(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1618,9 +1613,9 @@ func TestAvailableUnsupportedFs(t *testing.T) {
|
|||
|
||||
func TestChtimes(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
|
@ -1651,9 +1646,9 @@ func TestChtimes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1663,7 +1658,7 @@ func TestChown(t *testing.T) {
|
|||
if runtime.GOOS == osWindows {
|
||||
t.Skip("chown is not supported on Windows")
|
||||
}
|
||||
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1686,7 +1681,7 @@ func TestChown(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1697,9 +1692,9 @@ func TestChmod(t *testing.T) {
|
|||
t.Skip("chmod is partially supported on Windows")
|
||||
}
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
client, err := getFTPClient(user, true)
|
||||
|
@ -1733,9 +1728,9 @@ func TestChmod(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1743,9 +1738,9 @@ func TestChmod(t *testing.T) {
|
|||
|
||||
func TestCombineDisabled(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
client, err := getFTPClient(user, true)
|
||||
|
@ -1763,9 +1758,9 @@ func TestCombineDisabled(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1773,7 +1768,7 @@ func TestCombineDisabled(t *testing.T) {
|
|||
|
||||
func TestActiveModeDisabled(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClientImplicitTLS(user)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1809,7 +1804,7 @@ func TestActiveModeDisabled(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1817,7 +1812,7 @@ func TestActiveModeDisabled(t *testing.T) {
|
|||
|
||||
func TestSITEDisabled(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClientImplicitTLS(user)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -1832,7 +1827,7 @@ func TestSITEDisabled(t *testing.T) {
|
|||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1840,13 +1835,13 @@ func TestSITEDisabled(t *testing.T) {
|
|||
|
||||
func TestHASH(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestUserWithCryptFs()
|
||||
u.Username += "_crypt"
|
||||
cryptUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
cryptUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser, cryptUser} {
|
||||
client, err := getFTPClientImplicitTLS(user)
|
||||
|
@ -1891,13 +1886,13 @@ func TestHASH(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(cryptUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(cryptUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(cryptUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1905,9 +1900,9 @@ func TestHASH(t *testing.T) {
|
|||
|
||||
func TestCombine(t *testing.T) {
|
||||
u := getTestUser()
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
client, err := getFTPClientImplicitTLS(user)
|
||||
|
@ -1945,9 +1940,9 @@ func TestCombine(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
|
21
go.mod
|
@ -3,34 +3,39 @@ module github.com/drakkan/sftpgo
|
|||
go 1.15
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.74.0 // indirect
|
||||
cloud.google.com/go v0.75.0 // indirect
|
||||
cloud.google.com/go/storage v1.12.0
|
||||
github.com/Azure/azure-storage-blob-go v0.12.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b
|
||||
github.com/aws/aws-sdk-go v1.36.20
|
||||
github.com/aws/aws-sdk-go v1.36.28
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
|
||||
github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d
|
||||
github.com/fclairamb/ftpserverlib v0.12.0
|
||||
github.com/frankban/quicktest v1.11.2 // indirect
|
||||
github.com/go-chi/chi v1.5.1
|
||||
github.com/go-chi/jwtauth v1.1.1
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.1.4 // indirect
|
||||
github.com/google/uuid v1.1.5 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||
github.com/lestrrat-go/jwx v1.0.8
|
||||
github.com/lib/pq v1.9.0
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/miekg/dns v1.1.35 // indirect
|
||||
github.com/minio/sha256-simd v0.1.1
|
||||
github.com/minio/sio v0.2.1
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/otiai10/copy v1.4.2
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.3.3
|
||||
github.com/pkg/sftp v1.12.1-0.20201128220914-b5b6f3393fe9
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
github.com/prometheus/procfs v0.3.0 // indirect
|
||||
github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b
|
||||
github.com/rs/xid v1.2.1
|
||||
github.com/rs/zerolog v1.20.0
|
||||
|
@ -49,12 +54,16 @@ require (
|
|||
gocloud.dev v0.21.0
|
||||
gocloud.dev/secrets/hashivault v0.21.0
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/mod v0.4.1 // indirect
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad
|
||||
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 // indirect
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
|
||||
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee // indirect
|
||||
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 // indirect
|
||||
google.golang.org/api v0.36.0
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d // indirect
|
||||
google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f // indirect
|
||||
google.golang.org/grpc v1.35.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
|
64
go.sum
|
@ -17,8 +17,8 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY
|
|||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0 h1:kpgPA77kSSbjSs+fWHkPTxQ6J5Z2Qkruo5jfXEkHxNQ=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
|
@ -106,8 +106,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
|
|||
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.36.20 h1:IQr81xegCd40Xq21ZjFToKw9llaCzO1LRE75CgnvJ1Q=
|
||||
github.com/aws/aws-sdk-go v1.36.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.36.28 h1:JVRN7BZgwQ31SQCBwG5QM445+ynJU0ruKu+miFIijYY=
|
||||
github.com/aws/aws-sdk-go v1.36.28/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
|
@ -131,6 +131,7 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4
|
|||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
|
@ -176,6 +177,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
|||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
|
@ -196,6 +198,8 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC
|
|||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
|
||||
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
|
||||
github.com/go-chi/jwtauth v1.1.1 h1:CtUHwzvXUfZeZSbASLgzaTZQ8mL7p+vitX59NBTL1vY=
|
||||
github.com/go-chi/jwtauth v1.1.1/go.mod h1:znOWz9e5/GfBOKiZlOUoEfjSjUF+cLZO3GcpkoGXvFI=
|
||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
|
@ -210,6 +214,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
|||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
|
@ -290,7 +295,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
|
|||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
|
@ -298,8 +303,8 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3
|
|||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0=
|
||||
github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I=
|
||||
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
|
||||
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
|
||||
github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
|
||||
|
@ -410,6 +415,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.3 h1:2ABaTa5ifB1L90aoRMjaPa97p0WzzVe93Vggv8oZftw=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.3/go.mod h1:mU93bMXuG27/Y5erI5E9weqavpTX5qiVFZI4uXAX0xk=
|
||||
github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3 h1:e52qvXxpJPV/Kb2ovtuYgcRFjNmf9ntcn8BPIbpRM4k=
|
||||
github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY=
|
||||
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.0.6-0.20201127121120-26218808f029/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0=
|
||||
github.com/lestrrat-go/jwx v1.0.8 h1:Mj/2Ey9rkGx4w5IMQ2Q+9KLZn4cZoMgKrnMxi9eXE3k=
|
||||
github.com/lestrrat-go/jwx v1.0.8/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214zwZDdvho=
|
||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10=
|
||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8=
|
||||
github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
|
||||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
|
@ -450,8 +468,9 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
|
|||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks=
|
||||
github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -540,8 +559,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
|||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.3.0 h1:Uehi/mxLK0eiUc0H0++5tpMGTexB8wZ598MIgU8VpDM=
|
||||
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
|
@ -682,6 +702,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -690,8 +711,9 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
|
|||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201203001011-0b49973bad19/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 h1:BaN3BAqnopnKjvl+15DYP6LLrbBHfbfmlFYzmFj/Q9Q=
|
||||
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -722,6 +744,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -754,8 +777,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad h1:MCsdmFSdEd4UEa5TKS5JztCRHK/WtvNei1edOj5RSRo=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -764,8 +787,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
|||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -817,6 +841,7 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
|
@ -832,8 +857,8 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -911,9 +936,9 @@ google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6D
|
|||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d h1:HV9Z9qMhQEsdlvxNFELgQ11RkMzO3CMkjEySjCtuLes=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f h1:izedQ6yVIc5mZsRuXzmSreCOlzI0lCU1HpG8yEdMiKw=
|
||||
google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
|
@ -936,8 +961,9 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
|||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
211
httpd/api_admin.go
Normal file
|
@ -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
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -64,16 +65,21 @@ func addFolder(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
err = dataprovider.AddFolder(&folder)
|
||||
if err == nil {
|
||||
folder, err = dataprovider.GetFolderByPath(folder.MappedPath)
|
||||
if err == nil {
|
||||
render.JSON(w, r, folder)
|
||||
} else {
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
renderFolder(w, r, folder.MappedPath)
|
||||
}
|
||||
|
||||
func renderFolder(w http.ResponseWriter, r *http.Request, mappedPath string) {
|
||||
folder, err := dataprovider.GetFolderByPath(mappedPath)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)
|
||||
render.JSON(w, r.WithContext(ctx), folder)
|
||||
}
|
||||
|
||||
func deleteFolderByPath(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -87,15 +93,10 @@ func deleteFolderByPath(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
folder, err := dataprovider.GetFolderByPath(folderPath)
|
||||
err := dataprovider.DeleteFolder(folderPath)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
err = dataprovider.DeleteFolder(&folder)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "Folder deleted", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,13 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
logger.Debug(logSender, "", "backup restored, users: %v", len(dump.Users))
|
||||
if err = RestoreAdmins(dump.Admins, inputFile, mode); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(logSender, "", "backup restored, users: %v, folders: %v, admins: %vs",
|
||||
len(dump.Users), len(dump.Folders), len(dump.Admins))
|
||||
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
||||
}
|
||||
|
||||
|
@ -164,6 +170,33 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, scanQuota
|
|||
return nil
|
||||
}
|
||||
|
||||
// RestoreAdmins restores the specified admins
|
||||
func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) error {
|
||||
for _, admin := range admins {
|
||||
admin := admin // pin
|
||||
a, err := dataprovider.AdminExists(admin.Username)
|
||||
if err == nil {
|
||||
if mode == 1 {
|
||||
logger.Debug(logSender, "", "loaddata mode 1, existing admin %#v not updated", a.Username)
|
||||
continue
|
||||
}
|
||||
admin.ID = a.ID
|
||||
err = dataprovider.UpdateAdmin(&admin)
|
||||
admin.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "restoring existing admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
|
||||
} else {
|
||||
err = dataprovider.AddAdmin(&admin)
|
||||
admin.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "adding new admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreUsers restores the specified users
|
||||
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int) error {
|
||||
for _, user := range users {
|
||||
|
@ -176,14 +209,14 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
|
|||
}
|
||||
user.ID = u.ID
|
||||
err = dataprovider.UpdateUser(&user)
|
||||
user.Password = "[redacted]"
|
||||
user.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "restoring existing user: %+v, dump file: %#v, error: %v", user, inputFile, err)
|
||||
if mode == 2 && err == nil {
|
||||
disconnectUser(user.Username)
|
||||
}
|
||||
} else {
|
||||
err = dataprovider.AddUser(&user)
|
||||
user.Password = "[redacted]"
|
||||
user.Password = redactedSecret
|
||||
logger.Debug(logSender, "", "adding new user: %+v, dump file: %#v, error: %v", user, inputFile, err)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
|
@ -16,11 +16,11 @@ import (
|
|||
)
|
||||
|
||||
func getUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
limit := 100
|
||||
offset := 0
|
||||
order := dataprovider.OrderASC
|
||||
username := ""
|
||||
var err error
|
||||
if _, ok := r.URL.Query()["limit"]; ok {
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
|
@ -48,10 +48,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["username"]; ok {
|
||||
username = r.URL.Query().Get("username")
|
||||
}
|
||||
users, err := dataprovider.GetUsers(limit, offset, order, username)
|
||||
users, err := dataprovider.GetUsers(limit, offset, order)
|
||||
if err == nil {
|
||||
render.JSON(w, r, users)
|
||||
} else {
|
||||
|
@ -59,19 +56,23 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func getUserByID(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
func getUserByUsername(w http.ResponseWriter, r *http.Request) {
|
||||
username := getURLParam(r, "username")
|
||||
renderUser(w, r, username, http.StatusOK)
|
||||
}
|
||||
|
||||
func renderUser(w http.ResponseWriter, r *http.Request, username string, status int) {
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err != nil {
|
||||
err = errors.New("Invalid userID")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(userID)
|
||||
if err == nil {
|
||||
user.HideConfidentialData()
|
||||
render.JSON(w, r, user)
|
||||
if status != http.StatusOK {
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)
|
||||
render.JSON(w, r.WithContext(ctx), user)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
render.JSON(w, r, user)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,27 +117,18 @@ func addUser(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
err = dataprovider.AddUser(&user)
|
||||
if err == nil {
|
||||
user, err = dataprovider.UserExists(user.Username)
|
||||
if err == nil {
|
||||
user.HideConfidentialData()
|
||||
render.JSON(w, r, user)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
renderUser(w, r, user.Username, http.StatusCreated)
|
||||
}
|
||||
|
||||
func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
if err != nil {
|
||||
err = errors.New("Invalid userID")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
|
||||
username := getURLParam(r, "username")
|
||||
disconnect := 0
|
||||
if _, ok := r.URL.Query()["disconnect"]; ok {
|
||||
disconnect, err = strconv.Atoi(r.URL.Query().Get("disconnect"))
|
||||
|
@ -146,11 +138,12 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(userID)
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
userID := user.ID
|
||||
currentPermissions := user.Permissions
|
||||
currentS3AccessSecret := user.FsConfig.S3Config.AccessSecret
|
||||
currentAzAccountKey := user.FsConfig.AzBlobConfig.AccountKey
|
||||
|
@ -170,6 +163,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user.ID = userID
|
||||
user.Username = username
|
||||
user.SetEmptySecretsIfNil()
|
||||
// we use new Permissions if passed otherwise the old ones
|
||||
if len(user.Permissions) == 0 {
|
||||
|
@ -177,40 +172,26 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
updateEncryptedSecrets(&user, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials, currentCryptoPassphrase,
|
||||
currentSFTPPassword, currentSFTPKey)
|
||||
if user.ID != userID {
|
||||
sendAPIResponse(w, r, err, "user ID in request body does not match user ID in path parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = dataprovider.UpdateUser(&user)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
} else {
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, err, "User updated", http.StatusOK)
|
||||
if disconnect == 1 {
|
||||
disconnectUser(user.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
if err != nil {
|
||||
err = errors.New("Invalid userID")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(userID)
|
||||
username := getURLParam(r, "username")
|
||||
err := dataprovider.DeleteUser(username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
err = dataprovider.DeleteUser(&user)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "User deleted", http.StatusOK)
|
||||
disconnectUser(user.Username)
|
||||
}
|
||||
disconnectUser(username)
|
||||
}
|
||||
|
||||
func disconnectUser(username string) {
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
124
httpd/httpd.go
|
@ -1,19 +1,16 @@
|
|||
// Package httpd implements REST API and Web interface for SFTPGo.
|
||||
// REST API allows to manage users and quota and to get real time reports for the active connections
|
||||
// with possibility of forcibly closing a connection.
|
||||
// The OpenAPI 3 schema for the exposed API can be found inside the source tree:
|
||||
// https://github.com/drakkan/sftpgo/blob/master/httpd/schema/openapi.yaml
|
||||
// A basic Web interface to manage users and connections is provided too
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
|
@ -28,29 +25,38 @@ import (
|
|||
|
||||
const (
|
||||
logSender = "httpd"
|
||||
apiPrefix = "/api/v1"
|
||||
activeConnectionsPath = "/api/v1/connection"
|
||||
quotaScanPath = "/api/v1/quota_scan"
|
||||
quotaScanVFolderPath = "/api/v1/folder_quota_scan"
|
||||
userPath = "/api/v1/user"
|
||||
versionPath = "/api/v1/version"
|
||||
folderPath = "/api/v1/folder"
|
||||
serverStatusPath = "/api/v1/status"
|
||||
dumpDataPath = "/api/v1/dumpdata"
|
||||
loadDataPath = "/api/v1/loaddata"
|
||||
updateUsedQuotaPath = "/api/v1/quota_update"
|
||||
updateFolderUsedQuotaPath = "/api/v1/folder_quota_update"
|
||||
defenderBanTime = "/api/v1/defender/ban_time"
|
||||
defenderUnban = "/api/v1/defender/unban"
|
||||
defenderScore = "/api/v1/defender/score"
|
||||
metricsPath = "/metrics"
|
||||
tokenPath = "/api/v2/token"
|
||||
activeConnectionsPath = "/api/v2/connections"
|
||||
quotaScanPath = "/api/v2/quota-scans"
|
||||
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
|
||||
userPath = "/api/v2/users"
|
||||
versionPath = "/api/v2/version"
|
||||
folderPath = "/api/v2/folders"
|
||||
serverStatusPath = "/api/v2/status"
|
||||
dumpDataPath = "/api/v2/dumpdata"
|
||||
loadDataPath = "/api/v2/loaddata"
|
||||
updateUsedQuotaPath = "/api/v2/quota-update"
|
||||
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
|
||||
defenderBanTime = "/api/v2/defender/bantime"
|
||||
defenderUnban = "/api/v2/defender/unban"
|
||||
defenderScore = "/api/v2/defender/score"
|
||||
adminPath = "/api/v2/admins"
|
||||
adminPwdPath = "/api/v2/changepwd/admin"
|
||||
healthzPath = "/healthz"
|
||||
webBasePath = "/web"
|
||||
webLoginPath = "/web/login"
|
||||
webLogoutPath = "/web/logout"
|
||||
webUsersPath = "/web/users"
|
||||
webUserPath = "/web/user"
|
||||
webConnectionsPath = "/web/connections"
|
||||
webFoldersPath = "/web/folders"
|
||||
webFolderPath = "/web/folder"
|
||||
webStatusPath = "/web/status"
|
||||
webAdminsPath = "/web/admins"
|
||||
webAdminPath = "/web/admin"
|
||||
webScanVFolderPath = "/web/folder-quota-scans"
|
||||
webQuotaScanPath = "/web/quota-scans"
|
||||
webChangeAdminPwdPath = "/web/changepwd/admin"
|
||||
webStaticFilesPath = "/static"
|
||||
// MaxRestoreSize defines the max size for the loaddata input file
|
||||
MaxRestoreSize = 10485760 // 10 MB
|
||||
|
@ -59,12 +65,18 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
router *chi.Mux
|
||||
backupsPath string
|
||||
httpAuth common.HTTPAuthProvider
|
||||
certMgr *common.CertManager
|
||||
)
|
||||
|
||||
// Binding defines the configuration for a network listener
|
||||
type Binding struct {
|
||||
// The address to listen on. A blank value means listen on all available network interfaces.
|
||||
Address string `json:"address" mapstructure:"address"`
|
||||
// The port used for serving requests
|
||||
Port int `json:"port" mapstructure:"port"`
|
||||
}
|
||||
|
||||
type defenderStatus struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
@ -91,12 +103,6 @@ type Conf struct {
|
|||
StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
|
||||
// Path to the backup directory. This can be an absolute path or a path relative to the config dir
|
||||
BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
|
||||
// Path to a file used to store usernames and password for basic authentication.
|
||||
// This can be an absolute path or a path relative to the config dir.
|
||||
// We support HTTP basic authentication and the file format must conform to the one generated using the Apache
|
||||
// htpasswd tool. The supported password formats are bcrypt ($2y$ prefix) and md5 crypt ($apr1$ prefix).
|
||||
// If empty HTTP authentication is disabled
|
||||
AuthUserFile string `json:"auth_user_file" mapstructure:"auth_user_file"`
|
||||
// If files containing a certificate and matching private key for the server are provided the server will expect
|
||||
// HTTPS connections.
|
||||
// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
|
||||
|
@ -128,7 +134,7 @@ func (c Conf) Initialize(configDir string) error {
|
|||
backupsPath = getConfigPath(c.BackupsPath, configDir)
|
||||
staticFilesPath := getConfigPath(c.StaticFilesPath, configDir)
|
||||
templatesPath := getConfigPath(c.TemplatesPath, configDir)
|
||||
enableWebAdmin := len(staticFilesPath) > 0 || len(templatesPath) > 0
|
||||
enableWebAdmin := staticFilesPath != "" || templatesPath != ""
|
||||
if backupsPath == "" {
|
||||
return fmt.Errorf("Required directory is invalid, backup path %#v", backupsPath)
|
||||
}
|
||||
|
@ -136,11 +142,6 @@ func (c Conf) Initialize(configDir string) error {
|
|||
return fmt.Errorf("Required directory is invalid, static file path: %#v template path: %#v",
|
||||
staticFilesPath, templatesPath)
|
||||
}
|
||||
authUserFile := getConfigPath(c.AuthUserFile, configDir)
|
||||
httpAuth, err = common.NewBasicAuthProvider(authUserFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certificateFile := getConfigPath(c.CertificateFile, configDir)
|
||||
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
|
||||
if enableWebAdmin {
|
||||
|
@ -148,28 +149,18 @@ func (c Conf) Initialize(configDir string) error {
|
|||
} else {
|
||||
logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it")
|
||||
}
|
||||
initializeRouter(staticFilesPath, enableWebAdmin)
|
||||
httpServer := &http.Server{
|
||||
Handler: router,
|
||||
ReadTimeout: 60 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 16, // 64KB
|
||||
ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
|
||||
}
|
||||
if certificateFile != "" && certificateKeyFile != "" {
|
||||
certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &tls.Config{
|
||||
GetCertificate: certMgr.GetCertificateFunc(),
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
httpServer.TLSConfig = config
|
||||
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, logSender)
|
||||
}
|
||||
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, logSender)
|
||||
server := newHttpdServer(c.BindAddress, c.BindPort, staticFilesPath, enableWebAdmin)
|
||||
return server.listenAndServe()
|
||||
}
|
||||
|
||||
func isWebAdminRequest(r *http.Request) bool {
|
||||
return strings.HasPrefix(r.RequestURI, webBasePath+"/")
|
||||
}
|
||||
|
||||
// ReloadCertificateMgr reloads the certificate manager
|
||||
|
@ -202,3 +193,34 @@ func getServicesStatus() ServicesStatus {
|
|||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func getURLParam(r *http.Request, key string) string {
|
||||
v := chi.URLParam(r, key)
|
||||
unescaped, err := url.PathUnescape(v)
|
||||
if err != nil {
|
||||
return v
|
||||
}
|
||||
return unescaped
|
||||
}
|
||||
|
||||
func fileServer(r chi.Router, path string, root http.FileSystem) {
|
||||
if path != "/" && path[len(path)-1] != '/' {
|
||||
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
||||
path += "/"
|
||||
}
|
||||
path += "*"
|
||||
|
||||
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
|
||||
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// GetHTTPRouter returns an HTTP handler suitable to use for test cases
|
||||
func GetHTTPRouter() http.Handler {
|
||||
server := newHttpdServer("", 8080, "../static", true)
|
||||
server.initializeRouter()
|
||||
return server.router
|
||||
}
|
||||
|
|
2399
httpd/httpd_test.go
|
@ -1,33 +1,31 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/jwtauth"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
||||
const (
|
||||
invalidURL = "http://foo\x7f.com/"
|
||||
inactiveURL = "http://127.0.0.1:12345"
|
||||
)
|
||||
|
||||
func TestShouldBind(t *testing.T) {
|
||||
|
@ -55,455 +53,6 @@ func TestGetRespStatus(t *testing.T) {
|
|||
assert.Equal(t, http.StatusInternalServerError, respStatus)
|
||||
}
|
||||
|
||||
func TestCheckResponse(t *testing.T) {
|
||||
err := checkResponse(http.StatusOK, http.StatusCreated)
|
||||
assert.Error(t, err)
|
||||
err = checkResponse(http.StatusBadRequest, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCheckFolder(t *testing.T) {
|
||||
expected := &vfs.BaseVirtualFolder{}
|
||||
actual := &vfs.BaseVirtualFolder{}
|
||||
err := checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.ID = 1
|
||||
actual.ID = 2
|
||||
err = checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.ID = 2
|
||||
actual.ID = 2
|
||||
expected.MappedPath = "path"
|
||||
err = checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.MappedPath = ""
|
||||
expected.LastQuotaUpdate = 1
|
||||
err = checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.LastQuotaUpdate = 0
|
||||
expected.UsedQuotaFiles = 1
|
||||
err = checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.UsedQuotaFiles = 0
|
||||
expected.UsedQuotaSize = 1
|
||||
err = checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.UsedQuotaSize = 0
|
||||
expected.Users = append(expected.Users, "user1")
|
||||
err = checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Users = append(actual.Users, "user2")
|
||||
err = checkFolder(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Users = nil
|
||||
actual.Users = nil
|
||||
}
|
||||
|
||||
func TestCheckUser(t *testing.T) {
|
||||
expected := &dataprovider.User{}
|
||||
actual := &dataprovider.User{}
|
||||
actual.Password = "password"
|
||||
err := checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Password = ""
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.ID = 1
|
||||
actual.ID = 2
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.ID = 2
|
||||
actual.ID = 2
|
||||
expected.Permissions = make(map[string][]string)
|
||||
expected.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
|
||||
actual.Permissions = make(map[string][]string)
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Permissions["/"] = append(expected.Permissions["/"], dataprovider.PermRename)
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Permissions = make(map[string][]string)
|
||||
expected.Permissions["/somedir"] = []string{dataprovider.PermAny}
|
||||
actual.Permissions = make(map[string][]string)
|
||||
actual.Permissions["/otherdir"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Permissions = make(map[string][]string)
|
||||
actual.Permissions = make(map[string][]string)
|
||||
actual.FsConfig.Provider = dataprovider.S3FilesystemProvider
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.FsConfig.Provider = dataprovider.LocalFilesystemProvider
|
||||
expected.VirtualFolders = append(expected.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: os.TempDir(),
|
||||
},
|
||||
VirtualPath: "/vdir",
|
||||
})
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.VirtualFolders = append(actual.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: os.TempDir(),
|
||||
},
|
||||
VirtualPath: "/vdir1",
|
||||
})
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCompareUserFilters(t *testing.T) {
|
||||
expected := &dataprovider.User{}
|
||||
actual := &dataprovider.User{}
|
||||
actual.ID = 1
|
||||
expected.ID = 1
|
||||
expected.Filters.AllowedIP = []string{}
|
||||
actual.Filters.AllowedIP = []string{"192.168.1.2/32"}
|
||||
err := checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.AllowedIP = []string{"192.168.1.3/32"}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.AllowedIP = []string{}
|
||||
actual.Filters.AllowedIP = []string{}
|
||||
expected.Filters.DeniedIP = []string{}
|
||||
actual.Filters.DeniedIP = []string{"192.168.1.2/32"}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.DeniedIP = []string{"192.168.1.3/32"}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.DeniedIP = []string{}
|
||||
actual.Filters.DeniedIP = []string{}
|
||||
expected.Filters.DeniedLoginMethods = []string{}
|
||||
actual.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.DeniedLoginMethods = []string{}
|
||||
actual.Filters.DeniedLoginMethods = []string{}
|
||||
actual.Filters.DeniedProtocols = []string{common.ProtocolFTP}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Filters.DeniedProtocols = []string{}
|
||||
actual.Filters.DeniedProtocols = []string{}
|
||||
expected.Filters.MaxUploadFileSize = 0
|
||||
actual.Filters.MaxUploadFileSize = 100
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.MaxUploadFileSize = 0
|
||||
expected.Filters.FileExtensions = append(expected.Filters.FileExtensions, dataprovider.ExtensionsFilter{
|
||||
Path: "/",
|
||||
AllowedExtensions: []string{".jpg", ".png"},
|
||||
DeniedExtensions: []string{".zip", ".rar"},
|
||||
})
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FileExtensions = append(actual.Filters.FileExtensions, dataprovider.ExtensionsFilter{
|
||||
Path: "/sub",
|
||||
AllowedExtensions: []string{".jpg", ".png"},
|
||||
DeniedExtensions: []string{".zip", ".rar"},
|
||||
})
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{
|
||||
Path: "/",
|
||||
AllowedExtensions: []string{".jpg"},
|
||||
DeniedExtensions: []string{".zip", ".rar"},
|
||||
}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{
|
||||
Path: "/",
|
||||
AllowedExtensions: []string{".tiff", ".png"},
|
||||
DeniedExtensions: []string{".zip", ".rar"},
|
||||
}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FileExtensions[0] = dataprovider.ExtensionsFilter{
|
||||
Path: "/",
|
||||
AllowedExtensions: []string{".jpg", ".png"},
|
||||
DeniedExtensions: []string{".tar.gz", ".rar"},
|
||||
}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FileExtensions = nil
|
||||
actual.Filters.FilePatterns = nil
|
||||
expected.Filters.FileExtensions = nil
|
||||
expected.Filters.FilePatterns = nil
|
||||
expected.Filters.FilePatterns = append(expected.Filters.FilePatterns, dataprovider.PatternsFilter{
|
||||
Path: "/",
|
||||
AllowedPatterns: []string{"*.jpg", "*.png"},
|
||||
DeniedPatterns: []string{"*.zip", "*.rar"},
|
||||
})
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FilePatterns = append(actual.Filters.FilePatterns, dataprovider.PatternsFilter{
|
||||
Path: "/sub",
|
||||
AllowedPatterns: []string{"*.jpg", "*.png"},
|
||||
DeniedPatterns: []string{"*.zip", "*.rar"},
|
||||
})
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{
|
||||
Path: "/",
|
||||
AllowedPatterns: []string{"*.jpg"},
|
||||
DeniedPatterns: []string{"*.zip", "*.rar"},
|
||||
}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{
|
||||
Path: "/",
|
||||
AllowedPatterns: []string{"*.tiff", "*.png"},
|
||||
DeniedPatterns: []string{"*.zip", "*.rar"},
|
||||
}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.Filters.FilePatterns[0] = dataprovider.PatternsFilter{
|
||||
Path: "/",
|
||||
AllowedPatterns: []string{"*.jpg", "*.png"},
|
||||
DeniedPatterns: []string{"*.tar.gz", "*.rar"},
|
||||
}
|
||||
err = checkUser(expected, actual)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCompareUserFields(t *testing.T) {
|
||||
expected := &dataprovider.User{}
|
||||
actual := &dataprovider.User{}
|
||||
expected.Permissions = make(map[string][]string)
|
||||
actual.Permissions = make(map[string][]string)
|
||||
expected.Username = "test"
|
||||
err := compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Username = ""
|
||||
expected.HomeDir = "homedir"
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.HomeDir = ""
|
||||
expected.UID = 1
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.UID = 0
|
||||
expected.GID = 1
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.GID = 0
|
||||
expected.MaxSessions = 2
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.MaxSessions = 0
|
||||
expected.QuotaSize = 4096
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.QuotaSize = 0
|
||||
expected.QuotaFiles = 2
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.QuotaFiles = 0
|
||||
expected.Permissions["/"] = []string{dataprovider.PermCreateDirs}
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Permissions = nil
|
||||
expected.UploadBandwidth = 64
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.UploadBandwidth = 0
|
||||
expected.DownloadBandwidth = 128
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.DownloadBandwidth = 0
|
||||
expected.Status = 1
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.Status = 0
|
||||
expected.ExpirationDate = 123
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.ExpirationDate = 0
|
||||
expected.AdditionalInfo = "info"
|
||||
err = compareEqualsUserFields(expected, actual)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCompareUserFsConfig(t *testing.T) {
|
||||
secretString := "access secret"
|
||||
expected := &dataprovider.User{}
|
||||
actual := &dataprovider.User{}
|
||||
expected.SetEmptySecretsIfNil()
|
||||
actual.SetEmptySecretsIfNil()
|
||||
expected.FsConfig.Provider = dataprovider.S3FilesystemProvider
|
||||
err := compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.Provider = dataprovider.LocalFilesystemProvider
|
||||
expected.FsConfig.S3Config.Bucket = "bucket"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.Bucket = ""
|
||||
expected.FsConfig.S3Config.Region = "region"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.Region = ""
|
||||
expected.FsConfig.S3Config.AccessKey = "access key"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessKey = ""
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
secret, err := utils.EncryptData(secretString)
|
||||
assert.NoError(t, err)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
kmsSecret, err := kms.GetSecretFromCompatString(secret)
|
||||
assert.NoError(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret = kmsSecret
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, "", "", "")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "", "data")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "key", "")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret = nil
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
expected.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.Endpoint = ""
|
||||
expected.FsConfig.S3Config.StorageClass = "Standard"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.StorageClass = ""
|
||||
expected.FsConfig.S3Config.KeyPrefix = "somedir/subdir"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.KeyPrefix = ""
|
||||
expected.FsConfig.S3Config.UploadPartSize = 10
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.UploadPartSize = 0
|
||||
expected.FsConfig.S3Config.UploadConcurrency = 3
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.UploadConcurrency = 0
|
||||
expected.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("payload")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
|
||||
expected.FsConfig.SFTPConfig.Endpoint = "endpoint"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.SFTPConfig.Endpoint = ""
|
||||
expected.FsConfig.SFTPConfig.Username = "user"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.SFTPConfig.Username = ""
|
||||
expected.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftppwd")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
|
||||
expected.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret("fake key")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()
|
||||
expected.FsConfig.SFTPConfig.Prefix = "/home"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.SFTPConfig.Prefix = ""
|
||||
expected.FsConfig.SFTPConfig.Fingerprints = []string{"sha256:..."}
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.FsConfig.SFTPConfig.Fingerprints = []string{"sha256:different"}
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCompareUserGCSConfig(t *testing.T) {
|
||||
expected := &dataprovider.User{}
|
||||
actual := &dataprovider.User{}
|
||||
expected.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir"
|
||||
err := compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.GCSConfig.KeyPrefix = ""
|
||||
expected.FsConfig.GCSConfig.Bucket = "bucket"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.GCSConfig.Bucket = ""
|
||||
expected.FsConfig.GCSConfig.StorageClass = "Standard"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.GCSConfig.StorageClass = ""
|
||||
expected.FsConfig.GCSConfig.AutomaticCredentials = 1
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.GCSConfig.AutomaticCredentials = 0
|
||||
}
|
||||
|
||||
func TestCompareUserAzureConfig(t *testing.T) {
|
||||
expected := &dataprovider.User{}
|
||||
actual := &dataprovider.User{}
|
||||
expected.FsConfig.AzBlobConfig.Container = "a"
|
||||
err := compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.Container = ""
|
||||
expected.FsConfig.AzBlobConfig.AccountName = "aname"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.AccountName = ""
|
||||
expected.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusAWS, "payload", "", "")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
|
||||
expected.FsConfig.AzBlobConfig.Endpoint = "endpt"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.Endpoint = ""
|
||||
expected.FsConfig.AzBlobConfig.SASURL = "url"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.SASURL = ""
|
||||
expected.FsConfig.AzBlobConfig.UploadPartSize = 1
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.UploadPartSize = 0
|
||||
expected.FsConfig.AzBlobConfig.UploadConcurrency = 1
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.UploadConcurrency = 0
|
||||
expected.FsConfig.AzBlobConfig.KeyPrefix = "prefix/"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.KeyPrefix = ""
|
||||
expected.FsConfig.AzBlobConfig.UseEmulator = true
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.UseEmulator = false
|
||||
expected.FsConfig.AzBlobConfig.AccessTier = "Hot"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.AccessTier = ""
|
||||
}
|
||||
|
||||
func TestGCSWebInvalidFormFile(t *testing.T) {
|
||||
form := make(url.Values)
|
||||
form.Set("username", "test_username")
|
||||
|
@ -516,170 +65,313 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
|
|||
assert.EqualError(t, err, http.ErrNotMultipart.Error())
|
||||
}
|
||||
|
||||
func TestApiCallsWithBadURL(t *testing.T) {
|
||||
oldBaseURL := httpBaseURL
|
||||
oldAuthUsername := authUsername
|
||||
oldAuthPassword := authPassword
|
||||
SetBaseURLAndCredentials(invalidURL, oldAuthUsername, oldAuthPassword)
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
MappedPath: os.TempDir(),
|
||||
func TestInvalidToken(t *testing.T) {
|
||||
admin := dataprovider.Admin{
|
||||
Username: "admin",
|
||||
}
|
||||
u := dataprovider.User{}
|
||||
_, _, err := UpdateUser(u, http.StatusBadRequest, "")
|
||||
assert.Error(t, err)
|
||||
_, err = RemoveUser(u, http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, err = RemoveFolder(folder, http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetUsers(1, 0, "", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetFolders(1, 0, "", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
_, err = UpdateQuotaUsage(u, "", http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, err = UpdateFolderQuotaUsage(folder, "", http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = Dumpdata("backup.json", "", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
_, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetBanTime("", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetScore("", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword)
|
||||
errFake := errors.New("fake error")
|
||||
asJSON, err := json.Marshal(admin)
|
||||
assert.NoError(t, err)
|
||||
req, _ := http.NewRequest(http.MethodPut, path.Join(adminPath, admin.Username), bytes.NewBuffer(asJSON))
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("username", admin.Username)
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake))
|
||||
rr := httptest.NewRecorder()
|
||||
updateAdmin(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
rr = httptest.NewRecorder()
|
||||
deleteAdmin(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
|
||||
adminPwd := adminPwd{
|
||||
CurrentPassword: "old",
|
||||
NewPassword: "new",
|
||||
}
|
||||
asJSON, err = json.Marshal(adminPwd)
|
||||
assert.NoError(t, err)
|
||||
req, _ = http.NewRequest(http.MethodPut, "", bytes.NewBuffer(asJSON))
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
req = req.WithContext(context.WithValue(req.Context(), jwtauth.ErrorCtxKey, errFake))
|
||||
rr = httptest.NewRecorder()
|
||||
changeAdminPassword(rr, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
adm := getAdminFromToken(req)
|
||||
assert.Empty(t, adm.Username)
|
||||
}
|
||||
|
||||
func TestApiCallToNotListeningServer(t *testing.T) {
|
||||
oldBaseURL := httpBaseURL
|
||||
oldAuthUsername := authUsername
|
||||
oldAuthPassword := authPassword
|
||||
SetBaseURLAndCredentials(inactiveURL, oldAuthUsername, oldAuthPassword)
|
||||
u := dataprovider.User{}
|
||||
_, _, err := AddUser(u, http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
_, _, err = UpdateUser(u, http.StatusNotFound, "")
|
||||
assert.Error(t, err)
|
||||
_, err = RemoveUser(u, http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetUserByID(-1, http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetUsers(100, 0, "", http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, err = UpdateQuotaUsage(u, "", http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetQuotaScans(http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, err = StartQuotaScan(u, http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
MappedPath: os.TempDir(),
|
||||
}
|
||||
_, err = StartFolderQuotaScan(folder, http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = AddFolder(folder, http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, err = RemoveFolder(folder, http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetFolders(0, 0, "", http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, err = UpdateFolderQuotaUsage(folder, "", http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetFoldersQuotaScans(http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetConnections(http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetVersion(http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetStatus(http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, _, err = Dumpdata("backup.json", "0", http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, _, err = Loaddata("/tmp/backup.json", "", "", http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetBanTime("", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
_, _, err = GetScore("", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
err = UnbanIP("", http.StatusBadRequest)
|
||||
assert.Error(t, err)
|
||||
func TestUpdateWebAdminInvalidClaims(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
|
||||
SetBaseURLAndCredentials(oldBaseURL, oldAuthUsername, oldAuthPassword)
|
||||
rr := httptest.NewRecorder()
|
||||
admin := dataprovider.Admin{
|
||||
Username: "",
|
||||
Password: "password",
|
||||
}
|
||||
c := jwtTokenClaims{
|
||||
Username: admin.Username,
|
||||
Permissions: admin.Permissions,
|
||||
Signature: admin.GetSignature(),
|
||||
}
|
||||
token, err := c.createTokenResponse(server.tokenAuth)
|
||||
assert.NoError(t, err)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set("status", "1")
|
||||
req, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, "admin"), bytes.NewBuffer([]byte(form.Encode())))
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("username", "admin")
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleWebUpdateAdminPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
}
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
oldAuthUsername := authUsername
|
||||
oldAuthPassword := authPassword
|
||||
authUserFile := filepath.Join(os.TempDir(), "http_users.txt")
|
||||
authUserData := []byte("test1:$2y$05$bcHSED7aO1cfLto6ZdDBOOKzlwftslVhtpIkRhAtSa4GuLmk5mola\n")
|
||||
err := ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
httpAuth, _ = common.NewBasicAuthProvider(authUserFile)
|
||||
_, _, err = GetVersion(http.StatusUnauthorized)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test1", "password1")
|
||||
_, _, err = GetVersion(http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test1", "wrong_password")
|
||||
resp, _ := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(metricsPath), nil, "")
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
authUserData = append(authUserData, []byte("test2:$1$OtSSTL8b$bmaCqEksI1e7rnZSjsIDR1\n")...)
|
||||
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
|
||||
_, _, err = GetVersion(http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test2", "wrong_password")
|
||||
_, _, err = GetVersion(http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
authUserData = append(authUserData, []byte("test2:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...)
|
||||
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
|
||||
_, _, err = GetVersion(http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test2", "wrong_password")
|
||||
_, _, err = GetVersion(http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
authUserData = append(authUserData, []byte("test3:$apr1$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...)
|
||||
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test3", "wrong_password")
|
||||
_, _, err = GetVersion(http.StatusUnauthorized)
|
||||
assert.NoError(t, err)
|
||||
authUserData = append(authUserData, []byte("test4:$invalid$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\n")...)
|
||||
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test3", "password2")
|
||||
_, _, err = GetVersion(http.StatusUnauthorized)
|
||||
assert.NoError(t, err)
|
||||
if runtime.GOOS != osWindows {
|
||||
authUserData = append(authUserData, []byte("test5:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\n")...)
|
||||
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(authUserFile, 0001)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test5", "password2")
|
||||
_, _, err = GetVersion(http.StatusUnauthorized)
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(authUserFile, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
func TestCreateTokenError(t *testing.T) {
|
||||
server := httpdServer{
|
||||
tokenAuth: jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil),
|
||||
}
|
||||
authUserData = append(authUserData, []byte("\"foo\"bar\"\r\n")...)
|
||||
err = ioutil.WriteFile(authUserFile, authUserData, os.ModePerm)
|
||||
rr := httptest.NewRecorder()
|
||||
admin := dataprovider.Admin{
|
||||
Username: "admin",
|
||||
Password: "password",
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
|
||||
server.checkAddrAndSendToken(rr, req, admin)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
form := make(url.Values)
|
||||
form.Set("username", admin.Username)
|
||||
form.Set("password", admin.Password)
|
||||
req, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.RemoteAddr = "127.0.0.1:1234"
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
server.handleWebLoginPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
// req with no content type
|
||||
req, _ = http.NewRequest(http.MethodPost, webLoginPath, nil)
|
||||
rr = httptest.NewRecorder()
|
||||
server.handleWebLoginPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
// req with no POST body
|
||||
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%AO%GG", nil)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
server.handleWebLoginPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A1%G2", nil)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebAdminChangePwdPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A2%G3", nil)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
_, err := getAdminFromPostFields(req)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestJWTTokenValidation(t *testing.T) {
|
||||
tokenAuth := jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil)
|
||||
claims := make(map[string]interface{})
|
||||
claims["username"] = "admin"
|
||||
claims[jwt.ExpirationKey] = time.Now().UTC().Add(-1 * time.Hour)
|
||||
token, _, err := tokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, "test2", "password2")
|
||||
_, _, err = GetVersion(http.StatusUnauthorized)
|
||||
|
||||
r := GetHTTPRouter()
|
||||
fn := jwtAuthenticator(r)
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, userPath, nil)
|
||||
ctx := jwtauth.NewContext(req.Context(), token, nil)
|
||||
fn.ServeHTTP(rr, req.WithContext(ctx))
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
|
||||
fn = jwtAuthenticatorWeb(r)
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
|
||||
fn.ServeHTTP(rr, req.WithContext(ctx))
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
|
||||
errTest := errors.New("test error")
|
||||
permFn := checkPerm(dataprovider.PermAdminAny)
|
||||
fn = permFn(r)
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath, nil)
|
||||
ctx = jwtauth.NewContext(req.Context(), token, errTest)
|
||||
fn.ServeHTTP(rr, req.WithContext(ctx))
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
|
||||
permFn = checkPerm(dataprovider.PermAdminAny)
|
||||
fn = permFn(r)
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
|
||||
req.RequestURI = webUserPath
|
||||
ctx = jwtauth.NewContext(req.Context(), token, errTest)
|
||||
fn.ServeHTTP(rr, req.WithContext(ctx))
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestAdminAllowListConnAddr(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
admin := dataprovider.Admin{
|
||||
Filters: dataprovider.AdminFilters{
|
||||
AllowList: []string{"192.168.1.0/24"},
|
||||
},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
ctx := context.WithValue(req.Context(), connAddrKey, "127.0.0.1:4567")
|
||||
req.RemoteAddr = "192.168.1.16:1234"
|
||||
server.checkAddrAndSendToken(rr, req.WithContext(ctx), admin)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
func TestUpdateContextFromCookie(t *testing.T) {
|
||||
server := httpdServer{
|
||||
tokenAuth: jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil),
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
claims := make(map[string]interface{})
|
||||
claims["a"] = "b"
|
||||
token, _, err := server.tokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(authUserFile)
|
||||
|
||||
ctx := jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.updateContextFromCookie(req.WithContext(ctx))
|
||||
}
|
||||
|
||||
func TestCookieExpiration(t *testing.T) {
|
||||
server := httpdServer{
|
||||
tokenAuth: jwtauth.New("HS256", utils.GenerateRandomBytes(32), nil),
|
||||
}
|
||||
err := errors.New("test error")
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
ctx := jwtauth.NewContext(req.Context(), nil, err)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie := rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
claims := make(map[string]interface{})
|
||||
claims["a"] = "b"
|
||||
token, _, err := server.tokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
SetBaseURLAndCredentials(httpBaseURL, oldAuthUsername, oldAuthPassword)
|
||||
httpAuth, _ = common.NewBasicAuthProvider("")
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
admin := dataprovider.Admin{
|
||||
Username: "newtestadmin",
|
||||
Password: "password",
|
||||
Permissions: []string{dataprovider.PermAdminAny},
|
||||
}
|
||||
claims = make(map[string]interface{})
|
||||
claims[claimUsernameKey] = admin.Username
|
||||
claims[claimPermissionsKey] = admin.Permissions
|
||||
claims[jwt.SubjectKey] = admin.GetSignature()
|
||||
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
|
||||
token, _, err = server.tokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
admin.Status = 0
|
||||
err = dataprovider.AddAdmin(&admin)
|
||||
assert.NoError(t, err)
|
||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
admin.Status = 1
|
||||
admin.Filters.AllowList = []string{"172.16.1.0/24"}
|
||||
err = dataprovider.UpdateAdmin(&admin)
|
||||
assert.NoError(t, err)
|
||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
admin, err = dataprovider.AdminExists(admin.Username)
|
||||
assert.NoError(t, err)
|
||||
claims = make(map[string]interface{})
|
||||
claims[claimUsernameKey] = admin.Username
|
||||
claims[claimPermissionsKey] = admin.Permissions
|
||||
claims[jwt.SubjectKey] = admin.GetSignature()
|
||||
claims[jwt.ExpirationKey] = time.Now().Add(1 * time.Minute)
|
||||
token, _, err = server.tokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
req.RemoteAddr = "192.168.8.1:1234"
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
req.RemoteAddr = "172.16.1.2:1234"
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
ctx = context.WithValue(ctx, connAddrKey, "10.9.9.9")
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.Empty(t, cookie)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, tokenPath, nil)
|
||||
req.RemoteAddr = "172.16.1.12:4567"
|
||||
ctx = jwtauth.NewContext(req.Context(), token, nil)
|
||||
server.checkCookieExpiration(rr, req.WithContext(ctx))
|
||||
cookie = rr.Header().Get("Set-Cookie")
|
||||
assert.True(t, strings.HasPrefix(cookie, "jwt="))
|
||||
|
||||
err = dataprovider.DeleteAdmin(admin.Username)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetURLParam(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, adminPwdPath, nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("val", "testuser%C3%A0")
|
||||
rctx.URLParams.Add("inval", "testuser%C3%AO%GG")
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
escaped := getURLParam(req, "val")
|
||||
assert.Equal(t, "testuserà", escaped)
|
||||
escaped = getURLParam(req, "inval")
|
||||
assert.Equal(t, "testuser%C3%AO%GG", escaped)
|
||||
}
|
||||
|
||||
func TestChangePwdValidationErrors(t *testing.T) {
|
||||
err := doChangeAdminPassword(nil, "", "", "")
|
||||
require.Error(t, err)
|
||||
err = doChangeAdminPassword(nil, "a", "b", "c")
|
||||
require.Error(t, err)
|
||||
err = doChangeAdminPassword(nil, "a", "a", "a")
|
||||
require.Error(t, err)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPut, adminPwdPath, nil)
|
||||
err = doChangeAdminPassword(req, "currentpwd", "newpwd", "newpwd")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderUnexistingFolder(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, folderPath, nil)
|
||||
renderFolder(rr, req, "path not mapped")
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
}
|
||||
|
||||
func TestCloseConnectionHandler(t *testing.T) {
|
||||
|
|
95
httpd/middleware.go
Normal file
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
138
httpd/router.go
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -2,12 +2,12 @@ openapi: 3.0.3
|
|||
info:
|
||||
title: SFTPGo
|
||||
description: SFTPGo REST API
|
||||
version: 2.3.0
|
||||
version: 2.4.0
|
||||
|
||||
servers:
|
||||
- url: /api/v1
|
||||
- url: /api/v2
|
||||
security:
|
||||
- BasicAuth: []
|
||||
- BearerAuth: []
|
||||
paths:
|
||||
/healthz:
|
||||
get:
|
||||
|
@ -26,6 +26,29 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
example: ok
|
||||
/token:
|
||||
get:
|
||||
security:
|
||||
- BasicAuth: []
|
||||
tags:
|
||||
- token
|
||||
summary: Get an access token
|
||||
operationId: get_token
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/Token'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/version:
|
||||
get:
|
||||
tags:
|
||||
|
@ -47,7 +70,34 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/connection:
|
||||
/changepwd/admin:
|
||||
put:
|
||||
tags:
|
||||
- admins
|
||||
summary: Change the password for the logged in admin
|
||||
operationId: change_admin_password
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PwdChange'
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/ApiResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/connections:
|
||||
get:
|
||||
tags:
|
||||
- connections
|
||||
|
@ -70,7 +120,7 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/connection/{connectionID}:
|
||||
/connections/{connectionID}:
|
||||
delete:
|
||||
tags:
|
||||
- connections
|
||||
|
@ -102,7 +152,7 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/defender/ban_time:
|
||||
/defender/bantime:
|
||||
get:
|
||||
tags:
|
||||
- defender
|
||||
|
@ -197,7 +247,7 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/quota_scan:
|
||||
/quota-scans:
|
||||
get:
|
||||
tags:
|
||||
- quota
|
||||
|
@ -255,7 +305,7 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/quota_update:
|
||||
/quota-update:
|
||||
put:
|
||||
tags:
|
||||
- quota
|
||||
|
@ -305,7 +355,7 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/folder_quota_update:
|
||||
/folder-quota-update:
|
||||
put:
|
||||
tags:
|
||||
- quota
|
||||
|
@ -355,7 +405,7 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/folder_quota_scan:
|
||||
/folder-quota-scans:
|
||||
get:
|
||||
tags:
|
||||
- quota
|
||||
|
@ -413,7 +463,7 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/folder:
|
||||
/folders:
|
||||
get:
|
||||
tags:
|
||||
- folders
|
||||
|
@ -484,7 +534,7 @@ paths:
|
|||
schema:
|
||||
$ref : '#/components/schemas/BaseVirtualFolder'
|
||||
responses:
|
||||
200:
|
||||
201:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
|
@ -533,7 +583,193 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user:
|
||||
/admins:
|
||||
get:
|
||||
tags:
|
||||
- admins
|
||||
summary: Returns an array with one or more admins
|
||||
description: For security reasons hashed passwords are omitted in the response
|
||||
operationId: get_admins
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
required: false
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 500
|
||||
default: 100
|
||||
required: false
|
||||
description: The maximum number of items to return. Max value is 500, default is 100
|
||||
- in: query
|
||||
name: order
|
||||
required: false
|
||||
description: Ordering admins by username. Default ASC
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- ASC
|
||||
- DESC
|
||||
example: ASC
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref : '#/components/schemas/Admin'
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
post:
|
||||
tags:
|
||||
- admins
|
||||
summary: Adds a new admin
|
||||
operationId: add_admin
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/Admin'
|
||||
responses:
|
||||
201:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/Admin'
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/admins/{username}:
|
||||
get:
|
||||
tags:
|
||||
- admins
|
||||
summary: Find admin by username
|
||||
description: For security reasons the hashed password is omitted in the response
|
||||
operationId: get_admin_by_username
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: username of the admin to retrieve
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/Admin'
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
put:
|
||||
tags:
|
||||
- admins
|
||||
summary: Update an existing admin
|
||||
operationId: update_admin
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: username of the admin to update
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/Admin'
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: "User updated"
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
delete:
|
||||
tags:
|
||||
- admins
|
||||
summary: Delete an existing admin
|
||||
operationId: delete_admin
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: username of the admin to delete
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: "User deleted"
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/users:
|
||||
get:
|
||||
tags:
|
||||
- users
|
||||
|
@ -567,12 +803,6 @@ paths:
|
|||
- ASC
|
||||
- DESC
|
||||
example: ASC
|
||||
- in: query
|
||||
name: username
|
||||
required: false
|
||||
description: Filter by username, extact match case sensitive
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
|
@ -604,7 +834,7 @@ paths:
|
|||
schema:
|
||||
$ref : '#/components/schemas/User'
|
||||
responses:
|
||||
200:
|
||||
201:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
|
@ -620,21 +850,20 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/{userID}:
|
||||
/users/{username}:
|
||||
get:
|
||||
tags:
|
||||
- users
|
||||
summary: Find user by ID
|
||||
summary: Find user by username
|
||||
description: For security reasons the hashed password is omitted in the response
|
||||
operationId: get_user_by_id
|
||||
operationId: get_user_by_username
|
||||
parameters:
|
||||
- name: userID
|
||||
- name: username
|
||||
in: path
|
||||
description: ID of the user to retrieve
|
||||
description: username of the user to retrieve
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
|
@ -660,13 +889,12 @@ paths:
|
|||
summary: Update an existing user
|
||||
operationId: update_user
|
||||
parameters:
|
||||
- name: userID
|
||||
- name: username
|
||||
in: path
|
||||
description: ID of the user to update
|
||||
description: username of the user to update
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
type: string
|
||||
- in: query
|
||||
name: disconnect
|
||||
schema:
|
||||
|
@ -711,13 +939,12 @@ paths:
|
|||
summary: Delete an existing user
|
||||
operationId: delete_user
|
||||
parameters:
|
||||
- name: userID
|
||||
- name: username
|
||||
in: path
|
||||
description: ID of the user to delete
|
||||
description: username of the user to delete
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
|
@ -949,6 +1176,22 @@ components:
|
|||
minItems: 1
|
||||
minProperties: 1
|
||||
description: hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory ("/") are required
|
||||
AdminPermissions:
|
||||
type: string
|
||||
enum:
|
||||
- '*'
|
||||
- 'add_users'
|
||||
- 'edit_users'
|
||||
- 'del_users'
|
||||
- 'view_users'
|
||||
- 'view_conns'
|
||||
- 'close_conns'
|
||||
- 'view_status'
|
||||
- 'manage_admins'
|
||||
- 'quota_scans'
|
||||
- 'manage_system'
|
||||
- 'manage_defender'
|
||||
- 'view_defender'
|
||||
LoginMethods:
|
||||
type: string
|
||||
enum:
|
||||
|
@ -975,14 +1218,12 @@ components:
|
|||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: list of, case insensitive, allowed shell like file patterns.
|
||||
example: [ "*.jpg", "a*b?.png" ]
|
||||
denied_patterns:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: list of, case insensitive, denied shell like file patterns. Denied patterns are evaluated before the allowed ones
|
||||
example: [ "*.zip" ]
|
||||
ExtensionsFilter:
|
||||
|
@ -995,14 +1236,12 @@ components:
|
|||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: list of, case insensitive, allowed files extension. Shell like expansion is not supported so you have to specify `.jpg` and not `*.jpg`
|
||||
example: [ ".jpg", ".png" ]
|
||||
denied_extensions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones
|
||||
example: [ ".zip" ]
|
||||
UserFilters:
|
||||
|
@ -1012,44 +1251,37 @@ components:
|
|||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32"
|
||||
example: [ "192.0.2.0/24", "2001:db8::/32" ]
|
||||
denied_ip:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones
|
||||
example: [ "172.16.0.0/16" ]
|
||||
denied_login_methods:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LoginMethods'
|
||||
nullable: true
|
||||
description: if null or empty any available login method is allowed
|
||||
denied_protocols:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SupportedProtocols'
|
||||
nullable: true
|
||||
description: if null or empty any available protocol is allowed
|
||||
file_patterns:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PatternsFilter'
|
||||
nullable: true
|
||||
description: filters based on shell like file patterns. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be in the list of files. Please note that these restrictions can be easily bypassed
|
||||
file_extensions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ExtensionsFilter'
|
||||
nullable: true
|
||||
description: filters based on shell like patterns. Deprecated, use file_patterns. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be in the list of files. Please note that these restrictions can be easily bypassed
|
||||
max_upload_file_size:
|
||||
type: integer
|
||||
format: int64
|
||||
nullable: true
|
||||
description: maximum allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited. This restriction does not apply for SSH system commands such as `git` and `rsync`
|
||||
description: Additional restrictions
|
||||
Secret:
|
||||
|
@ -1106,7 +1338,6 @@ components:
|
|||
required:
|
||||
- bucket
|
||||
- region
|
||||
nullable: true
|
||||
description: S3 Compatible Object Storage configuration details
|
||||
GCSConfig:
|
||||
type: object
|
||||
|
@ -1118,7 +1349,6 @@ components:
|
|||
$ref: '#/components/schemas/Secret'
|
||||
automatic_credentials:
|
||||
type: integer
|
||||
nullable: true
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
|
@ -1134,7 +1364,6 @@ components:
|
|||
example: folder/subfolder/
|
||||
required:
|
||||
- bucket
|
||||
nullable: true
|
||||
description: Google Cloud Storage configuration details. The "credentials" field must be populated only when adding/updating a user. It will be always omitted, since there are sensitive data, when you search/get users
|
||||
AzureBlobFsConfig:
|
||||
type: object
|
||||
|
@ -1171,7 +1400,6 @@ components:
|
|||
example: folder/subfolder/
|
||||
use_emulator:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Azure Blob Storage configuration details
|
||||
CryptFsConfig:
|
||||
type: object
|
||||
|
@ -1253,7 +1481,6 @@ components:
|
|||
description: Last quota update as unix timestamp in milliseconds
|
||||
users:
|
||||
type: array
|
||||
nullable: true
|
||||
items:
|
||||
type: string
|
||||
description: list of usernames associated with this virtual folder
|
||||
|
@ -1303,13 +1530,12 @@ components:
|
|||
description: expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration
|
||||
password:
|
||||
type: string
|
||||
nullable: true
|
||||
format: password
|
||||
description: password or public key/SSH user certificate are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
|
||||
public_keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: a password or at least one public key/SSH user certificate are mandatory.
|
||||
home_dir:
|
||||
type: string
|
||||
|
@ -1318,7 +1544,6 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VirtualFolder'
|
||||
nullable: true
|
||||
description: mapping between virtual SFTPGo paths and filesystem paths outside the user home directory. Supported for local filesystem only. If one or more of the specified folders are not inside the dataprovider they will be automatically created. You have to create the folder on the filesystem yourself
|
||||
uid:
|
||||
type: integer
|
||||
|
@ -1379,6 +1604,50 @@ components:
|
|||
additional_info:
|
||||
type: string
|
||||
description: Free form text field for external systems
|
||||
AdminFilters:
|
||||
type: object
|
||||
properties:
|
||||
allow_list:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32"
|
||||
example: [ "192.0.2.0/24", "2001:db8::/32" ]
|
||||
Admin:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 1
|
||||
status:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
description: >
|
||||
status:
|
||||
* `0` user is disabled, login is not allowed
|
||||
* `1` user is enabled
|
||||
username:
|
||||
type: string
|
||||
description: username is unique
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
description: Admin password. For security reasons this field is omitted when you search/get admins
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
permissions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AdminPermissions'
|
||||
filters:
|
||||
$ref: '#/components/schemas/AdminFilters'
|
||||
additional_info:
|
||||
type: string
|
||||
description: Free form text field
|
||||
Transfer:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1409,7 +1678,6 @@ components:
|
|||
description: unique connection identifier
|
||||
client_version:
|
||||
type: string
|
||||
nullable: true
|
||||
description: client version
|
||||
remote_address:
|
||||
type: string
|
||||
|
@ -1420,7 +1688,6 @@ components:
|
|||
description: connection time as unix timestamp in milliseconds
|
||||
command:
|
||||
type: string
|
||||
nullable: true
|
||||
description: SSH/FTP command or WebDAV method
|
||||
last_activity:
|
||||
type: integer
|
||||
|
@ -1436,7 +1703,6 @@ components:
|
|||
- DAV
|
||||
active_transfers:
|
||||
type: array
|
||||
nullable: true
|
||||
items:
|
||||
$ref : '#/components/schemas/Transfer'
|
||||
QuotaScan:
|
||||
|
@ -1602,6 +1868,13 @@ components:
|
|||
score:
|
||||
type: integer
|
||||
description: if 0 the host is not listed
|
||||
PwdChange:
|
||||
type: object
|
||||
properties:
|
||||
current_password:
|
||||
type: string
|
||||
new_password:
|
||||
type: string
|
||||
ApiResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1610,7 +1883,6 @@ components:
|
|||
description: message, can be empty
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: error description if any
|
||||
VersionInfo:
|
||||
type: object
|
||||
|
@ -1626,7 +1898,19 @@ components:
|
|||
items:
|
||||
type: string
|
||||
description: Features for the current build. Available features are "portable", "bolt", "mysql", "sqlite", "pgsql", "s3", "gcs", "metrics". If a feature is available it has a "+" prefix, otherwise a "-" prefix
|
||||
Token:
|
||||
type: object
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
securitySchemes:
|
||||
BasicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
|
346
httpd/server.go
Normal file
|
@ -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))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
386
httpd/web.go
|
@ -6,14 +6,13 @@ import (
|
|||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
|
@ -26,16 +25,23 @@ const (
|
|||
templateBase = "base.html"
|
||||
templateUsers = "users.html"
|
||||
templateUser = "user.html"
|
||||
templateAdmins = "admins.html"
|
||||
templateAdmin = "admin.html"
|
||||
templateConnections = "connections.html"
|
||||
templateFolders = "folders.html"
|
||||
templateFolder = "folder.html"
|
||||
templateMessage = "message.html"
|
||||
templateStatus = "status.html"
|
||||
templateLogin = "login.html"
|
||||
templateChangePwd = "changepwd.html"
|
||||
pageUsersTitle = "Users"
|
||||
pageAdminsTitle = "Admins"
|
||||
pageConnectionsTitle = "Connections"
|
||||
pageStatusTitle = "Status"
|
||||
pageFoldersTitle = "Folders"
|
||||
pageChangePwdTitle = "Change password"
|
||||
page400Title = "Bad request"
|
||||
page403Title = "Forbidden"
|
||||
page404Title = "Not found"
|
||||
page404Body = "The page you are looking for does not exist."
|
||||
page500Title = "Internal Server Error"
|
||||
|
@ -54,20 +60,23 @@ type basePage struct {
|
|||
CurrentURL string
|
||||
UsersURL string
|
||||
UserURL string
|
||||
APIUserURL string
|
||||
APIConnectionsURL string
|
||||
APIQuotaScanURL string
|
||||
AdminsURL string
|
||||
AdminURL string
|
||||
QuotaScanURL string
|
||||
ConnectionsURL string
|
||||
FoldersURL string
|
||||
FolderURL string
|
||||
APIFoldersURL string
|
||||
APIFolderQuotaScanURL string
|
||||
LogoutURL string
|
||||
ChangeAdminPwdURL string
|
||||
FolderQuotaScanURL string
|
||||
StatusURL string
|
||||
UsersTitle string
|
||||
AdminsTitle string
|
||||
ConnectionsTitle string
|
||||
FoldersTitle string
|
||||
StatusTitle string
|
||||
Version string
|
||||
LoggedAdmin *dataprovider.Admin
|
||||
}
|
||||
|
||||
type usersPage struct {
|
||||
|
@ -75,6 +84,11 @@ type usersPage struct {
|
|||
Users []dataprovider.User
|
||||
}
|
||||
|
||||
type adminsPage struct {
|
||||
basePage
|
||||
Admins []dataprovider.Admin
|
||||
}
|
||||
|
||||
type foldersPage struct {
|
||||
basePage
|
||||
Folders []vfs.BaseVirtualFolder
|
||||
|
@ -103,6 +117,18 @@ type userPage struct {
|
|||
IsAdd bool
|
||||
}
|
||||
|
||||
type adminPage struct {
|
||||
basePage
|
||||
Admin *dataprovider.Admin
|
||||
Error string
|
||||
IsAdd bool
|
||||
}
|
||||
|
||||
type changePwdPage struct {
|
||||
basePage
|
||||
Error string
|
||||
}
|
||||
|
||||
type folderPage struct {
|
||||
basePage
|
||||
Folder vfs.BaseVirtualFolder
|
||||
|
@ -115,6 +141,12 @@ type messagePage struct {
|
|||
Success string
|
||||
}
|
||||
|
||||
type loginPage struct {
|
||||
CurrentURL string
|
||||
Version string
|
||||
Error string
|
||||
}
|
||||
|
||||
func loadTemplates(templatesPath string) {
|
||||
usersPaths := []string{
|
||||
filepath.Join(templatesPath, templateBase),
|
||||
|
@ -124,6 +156,18 @@ func loadTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateBase),
|
||||
filepath.Join(templatesPath, templateUser),
|
||||
}
|
||||
adminsPaths := []string{
|
||||
filepath.Join(templatesPath, templateBase),
|
||||
filepath.Join(templatesPath, templateAdmins),
|
||||
}
|
||||
adminPaths := []string{
|
||||
filepath.Join(templatesPath, templateBase),
|
||||
filepath.Join(templatesPath, templateAdmin),
|
||||
}
|
||||
changePwdPaths := []string{
|
||||
filepath.Join(templatesPath, templateBase),
|
||||
filepath.Join(templatesPath, templateChangePwd),
|
||||
}
|
||||
connectionsPaths := []string{
|
||||
filepath.Join(templatesPath, templateBase),
|
||||
filepath.Join(templatesPath, templateConnections),
|
||||
|
@ -144,43 +188,57 @@ func loadTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateBase),
|
||||
filepath.Join(templatesPath, templateStatus),
|
||||
}
|
||||
loginPath := []string{
|
||||
filepath.Join(templatesPath, templateLogin),
|
||||
}
|
||||
usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
|
||||
userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
|
||||
adminsTmpl := utils.LoadTemplate(template.ParseFiles(adminsPaths...))
|
||||
adminTmpl := utils.LoadTemplate(template.ParseFiles(adminPaths...))
|
||||
connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...))
|
||||
messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...))
|
||||
foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...))
|
||||
folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...))
|
||||
statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...))
|
||||
loginTmpl := utils.LoadTemplate(template.ParseFiles(loginPath...))
|
||||
changePwdTmpl := utils.LoadTemplate(template.ParseFiles(changePwdPaths...))
|
||||
|
||||
templates[templateUsers] = usersTmpl
|
||||
templates[templateUser] = userTmpl
|
||||
templates[templateAdmins] = adminsTmpl
|
||||
templates[templateAdmin] = adminTmpl
|
||||
templates[templateConnections] = connectionsTmpl
|
||||
templates[templateMessage] = messageTmpl
|
||||
templates[templateFolders] = foldersTmpl
|
||||
templates[templateFolder] = folderTmpl
|
||||
templates[templateStatus] = statusTmpl
|
||||
templates[templateLogin] = loginTmpl
|
||||
templates[templateChangePwd] = changePwdTmpl
|
||||
}
|
||||
|
||||
func getBasePageData(title, currentURL string) basePage {
|
||||
func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
||||
return basePage{
|
||||
Title: title,
|
||||
CurrentURL: currentURL,
|
||||
UsersURL: webUsersPath,
|
||||
UserURL: webUserPath,
|
||||
AdminsURL: webAdminsPath,
|
||||
AdminURL: webAdminPath,
|
||||
FoldersURL: webFoldersPath,
|
||||
FolderURL: webFolderPath,
|
||||
APIUserURL: userPath,
|
||||
APIConnectionsURL: activeConnectionsPath,
|
||||
APIQuotaScanURL: quotaScanPath,
|
||||
APIFoldersURL: folderPath,
|
||||
APIFolderQuotaScanURL: quotaScanVFolderPath,
|
||||
LogoutURL: webLogoutPath,
|
||||
ChangeAdminPwdURL: webChangeAdminPwdPath,
|
||||
QuotaScanURL: webQuotaScanPath,
|
||||
ConnectionsURL: webConnectionsPath,
|
||||
StatusURL: webStatusPath,
|
||||
FolderQuotaScanURL: webScanVFolderPath,
|
||||
UsersTitle: pageUsersTitle,
|
||||
AdminsTitle: pageAdminsTitle,
|
||||
ConnectionsTitle: pageConnectionsTitle,
|
||||
FoldersTitle: pageFoldersTitle,
|
||||
StatusTitle: pageStatusTitle,
|
||||
Version: version.GetAsString(),
|
||||
LoggedAdmin: getAdminFromToken(r),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,16 +249,16 @@ func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
|
|||
}
|
||||
}
|
||||
|
||||
func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) {
|
||||
func renderMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
|
||||
var errorString string
|
||||
if len(body) > 0 {
|
||||
if body != "" {
|
||||
errorString = body + " "
|
||||
}
|
||||
if err != nil {
|
||||
errorString += err.Error()
|
||||
}
|
||||
data := messagePage{
|
||||
basePage: getBasePageData(title, ""),
|
||||
basePage: getBasePageData(title, "", r),
|
||||
Error: errorString,
|
||||
Success: message,
|
||||
}
|
||||
|
@ -208,22 +266,51 @@ func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int
|
|||
renderTemplate(w, templateMessage, data)
|
||||
}
|
||||
|
||||
func renderInternalServerErrorPage(w http.ResponseWriter, err error) {
|
||||
renderMessagePage(w, page500Title, page500Body, http.StatusInternalServerError, err, "")
|
||||
func renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
renderMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
|
||||
}
|
||||
|
||||
func renderBadRequestPage(w http.ResponseWriter, err error) {
|
||||
renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "")
|
||||
func renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
renderMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
|
||||
}
|
||||
|
||||
func renderNotFoundPage(w http.ResponseWriter, err error) {
|
||||
renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "")
|
||||
func renderForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
|
||||
renderMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
|
||||
}
|
||||
|
||||
func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
|
||||
func renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
|
||||
}
|
||||
|
||||
func renderChangePwdPage(w http.ResponseWriter, r *http.Request, error string) {
|
||||
data := changePwdPage{
|
||||
basePage: getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r),
|
||||
Error: error,
|
||||
}
|
||||
|
||||
renderTemplate(w, templateChangePwd, data)
|
||||
}
|
||||
|
||||
func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
|
||||
error string, isAdd bool) {
|
||||
currentURL := webAdminPath
|
||||
if !isAdd {
|
||||
currentURL = fmt.Sprintf("%v/%v", webAdminPath, url.PathEscape(admin.Username))
|
||||
}
|
||||
data := adminPage{
|
||||
basePage: getBasePageData("Add a new user", currentURL, r),
|
||||
Admin: admin,
|
||||
Error: error,
|
||||
IsAdd: isAdd,
|
||||
}
|
||||
|
||||
renderTemplate(w, templateAdmin, data)
|
||||
}
|
||||
|
||||
func renderAddUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) {
|
||||
user.SetEmptySecretsIfNil()
|
||||
data := userPage{
|
||||
basePage: getBasePageData("Add a new user", webUserPath),
|
||||
basePage: getBasePageData("Add a new user", webUserPath, r),
|
||||
IsAdd: true,
|
||||
Error: error,
|
||||
User: user,
|
||||
|
@ -236,10 +323,10 @@ func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error stri
|
|||
renderTemplate(w, templateUser, data)
|
||||
}
|
||||
|
||||
func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
|
||||
func renderUpdateUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) {
|
||||
user.SetEmptySecretsIfNil()
|
||||
data := userPage{
|
||||
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
|
||||
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username)), r),
|
||||
IsAdd: false,
|
||||
Error: error,
|
||||
User: user,
|
||||
|
@ -252,9 +339,9 @@ func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error s
|
|||
renderTemplate(w, templateUser, data)
|
||||
}
|
||||
|
||||
func renderAddFolderPage(w http.ResponseWriter, folder vfs.BaseVirtualFolder, error string) {
|
||||
func renderAddFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder, error string) {
|
||||
data := folderPage{
|
||||
basePage: getBasePageData("Add a new folder", webFolderPath),
|
||||
basePage: getBasePageData("Add a new folder", webFolderPath, r),
|
||||
Error: error,
|
||||
Folder: folder,
|
||||
}
|
||||
|
@ -571,6 +658,26 @@ func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, er
|
|||
return fs, nil
|
||||
}
|
||||
|
||||
func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
|
||||
var admin dataprovider.Admin
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return admin, err
|
||||
}
|
||||
status, err := strconv.Atoi(r.Form.Get("status"))
|
||||
if err != nil {
|
||||
return admin, err
|
||||
}
|
||||
admin.Username = r.Form.Get("username")
|
||||
admin.Password = r.Form.Get("password")
|
||||
admin.Permissions = r.Form["permissions"]
|
||||
admin.Email = r.Form.Get("email")
|
||||
admin.Status = status
|
||||
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||
admin.AdditionalInfo = r.Form.Get("additional_info")
|
||||
return admin, nil
|
||||
}
|
||||
|
||||
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
||||
var user dataprovider.User
|
||||
err := r.ParseMultipartForm(maxRequestSize)
|
||||
|
@ -649,6 +756,152 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
return user, err
|
||||
}
|
||||
|
||||
func renderLoginPage(w http.ResponseWriter, error string) {
|
||||
data := loginPage{
|
||||
CurrentURL: webLoginPath,
|
||||
Version: version.Get().Version,
|
||||
Error: error,
|
||||
}
|
||||
renderTemplate(w, templateLogin, data)
|
||||
}
|
||||
|
||||
func handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {
|
||||
renderChangePwdPage(w, r, "")
|
||||
}
|
||||
|
||||
func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
renderChangePwdPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
|
||||
r.Form.Get("new_password2"))
|
||||
if err != nil {
|
||||
renderChangePwdPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
handleWebLogout(w, r)
|
||||
}
|
||||
|
||||
func handleWebLogout(w http.ResponseWriter, r *http.Request) {
|
||||
c := jwtTokenClaims{}
|
||||
c.removeCookie(w)
|
||||
|
||||
http.Redirect(w, r, webLoginPath, http.StatusFound)
|
||||
}
|
||||
|
||||
func handleWebLogin(w http.ResponseWriter, r *http.Request) {
|
||||
renderLoginPage(w, "")
|
||||
}
|
||||
|
||||
func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
var err error
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
|
||||
if err != nil {
|
||||
limit = defaultQueryLimit
|
||||
}
|
||||
}
|
||||
admins := make([]dataprovider.Admin, 0, limit)
|
||||
for {
|
||||
a, err := dataprovider.GetAdmins(limit, len(admins), dataprovider.OrderASC)
|
||||
if err != nil {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
admins = append(admins, a...)
|
||||
if len(a) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
data := adminsPage{
|
||||
basePage: getBasePageData(pageAdminsTitle, webAdminsPath, r),
|
||||
Admins: admins,
|
||||
}
|
||||
renderTemplate(w, templateAdmins, data)
|
||||
}
|
||||
|
||||
func handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
|
||||
admin := &dataprovider.Admin{Status: 1}
|
||||
renderAddUpdateAdminPage(w, r, admin, "", true)
|
||||
}
|
||||
|
||||
func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
|
||||
username := getURLParam(r, "username")
|
||||
admin, err := dataprovider.AdminExists(username)
|
||||
if err == nil {
|
||||
renderAddUpdateAdminPage(w, r, &admin, "", false)
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
admin, err := getAdminFromPostFields(r)
|
||||
if err != nil {
|
||||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddAdmin(&admin)
|
||||
if err != nil {
|
||||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
username := getURLParam(r, "username")
|
||||
admin, err := dataprovider.AdminExists(username)
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, r, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedAdmin, err := getAdminFromPostFields(r)
|
||||
if err != nil {
|
||||
renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
|
||||
return
|
||||
}
|
||||
updatedAdmin.ID = admin.ID
|
||||
updatedAdmin.Username = admin.Username
|
||||
if updatedAdmin.Password == "" {
|
||||
updatedAdmin.Password = admin.Password
|
||||
}
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderAddUpdateAdminPage(w, r, &updatedAdmin, fmt.Sprintf("Invalid token claims: %v", err), false)
|
||||
return
|
||||
}
|
||||
if username == claims.Username {
|
||||
if claims.isCriticalPermRemoved(updatedAdmin.Permissions) {
|
||||
renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot remove these permissions to yourself", false)
|
||||
return
|
||||
}
|
||||
if updatedAdmin.Status == 0 {
|
||||
renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false)
|
||||
return
|
||||
}
|
||||
}
|
||||
err = dataprovider.UpdateAdmin(&updatedAdmin)
|
||||
if err != nil {
|
||||
renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
|
@ -660,9 +913,9 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
users := make([]dataprovider.User, 0, limit)
|
||||
for {
|
||||
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC, "")
|
||||
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC)
|
||||
if err != nil {
|
||||
renderInternalServerErrorPage(w, err)
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
users = append(users, u...)
|
||||
|
@ -671,52 +924,44 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
data := usersPage{
|
||||
basePage: getBasePageData(pageUsersTitle, webUsersPath),
|
||||
basePage: getBasePageData(pageUsersTitle, webUsersPath, r),
|
||||
Users: users,
|
||||
}
|
||||
renderTemplate(w, templateUsers, data)
|
||||
}
|
||||
|
||||
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("cloneFromId") != "" {
|
||||
id, err := strconv.ParseInt(r.URL.Query().Get("cloneFromId"), 10, 64)
|
||||
if err != nil {
|
||||
renderBadRequestPage(w, err)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(id)
|
||||
if r.URL.Query().Get("cloneFrom") != "" {
|
||||
username := r.URL.Query().Get("cloneFrom")
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err == nil {
|
||||
user.ID = 0
|
||||
user.Username = ""
|
||||
if err := user.DecryptSecrets(); err != nil {
|
||||
renderInternalServerErrorPage(w, err)
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
renderAddUserPage(w, user, "")
|
||||
renderAddUserPage(w, r, user, "")
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, err)
|
||||
renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
renderInternalServerErrorPage(w, err)
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
}
|
||||
} else {
|
||||
user := dataprovider.User{Status: 1}
|
||||
renderAddUserPage(w, user, "")
|
||||
renderAddUserPage(w, r, user, "")
|
||||
}
|
||||
}
|
||||
|
||||
func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
if err != nil {
|
||||
renderBadRequestPage(w, err)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(id)
|
||||
username := getURLParam(r, "username")
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err == nil {
|
||||
renderUpdateUserPage(w, user, "")
|
||||
renderUpdateUserPage(w, r, user, "")
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, err)
|
||||
renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
renderInternalServerErrorPage(w, err)
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -724,40 +969,37 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
user, err := getUserFromPostFields(r)
|
||||
if err != nil {
|
||||
renderAddUserPage(w, user, err.Error())
|
||||
renderAddUserPage(w, r, user, err.Error())
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddUser(&user)
|
||||
if err == nil {
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
} else {
|
||||
renderAddUserPage(w, user, err.Error())
|
||||
renderAddUserPage(w, r, user, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
|
||||
if err != nil {
|
||||
renderBadRequestPage(w, err)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(id)
|
||||
username := getURLParam(r, "username")
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, err)
|
||||
renderNotFoundPage(w, r, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
renderInternalServerErrorPage(w, err)
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
updatedUser, err := getUserFromPostFields(r)
|
||||
if err != nil {
|
||||
renderUpdateUserPage(w, user, err.Error())
|
||||
renderUpdateUserPage(w, r, user, err.Error())
|
||||
return
|
||||
}
|
||||
updatedUser.ID = user.ID
|
||||
updatedUser.Username = user.Username
|
||||
updatedUser.SetEmptySecretsIfNil()
|
||||
if len(updatedUser.Password) == 0 {
|
||||
if updatedUser.Password == "" {
|
||||
updatedUser.Password = user.Password
|
||||
}
|
||||
updateEncryptedSecrets(&updatedUser, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
|
||||
|
@ -771,13 +1013,13 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
} else {
|
||||
renderUpdateUserPage(w, user, err.Error())
|
||||
renderUpdateUserPage(w, r, user, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
data := statusPage{
|
||||
basePage: getBasePageData(pageStatusTitle, webStatusPath),
|
||||
basePage: getBasePageData(pageStatusTitle, webStatusPath, r),
|
||||
Status: getServicesStatus(),
|
||||
}
|
||||
renderTemplate(w, templateStatus, data)
|
||||
|
@ -786,14 +1028,14 @@ func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
|
|||
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
|
||||
connectionStats := common.Connections.GetStats()
|
||||
data := connectionsPage{
|
||||
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath),
|
||||
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
|
||||
Connections: connectionStats,
|
||||
}
|
||||
renderTemplate(w, templateConnections, data)
|
||||
}
|
||||
|
||||
func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
|
||||
renderAddFolderPage(w, vfs.BaseVirtualFolder{}, "")
|
||||
renderAddFolderPage(w, r, vfs.BaseVirtualFolder{}, "")
|
||||
}
|
||||
|
||||
func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -801,7 +1043,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
folder := vfs.BaseVirtualFolder{}
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
renderAddFolderPage(w, folder, err.Error())
|
||||
renderAddFolderPage(w, r, folder, err.Error())
|
||||
return
|
||||
}
|
||||
folder.MappedPath = r.Form.Get("mapped_path")
|
||||
|
@ -810,7 +1052,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
if err == nil {
|
||||
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
|
||||
} else {
|
||||
renderAddFolderPage(w, folder, err.Error())
|
||||
renderAddFolderPage(w, r, folder, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -827,7 +1069,7 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
|
|||
for {
|
||||
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, "")
|
||||
if err != nil {
|
||||
renderInternalServerErrorPage(w, err)
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
folders = append(folders, f...)
|
||||
|
@ -837,7 +1079,7 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
data := foldersPage{
|
||||
basePage: getBasePageData(pageFoldersTitle, webFoldersPath),
|
||||
basePage: getBasePageData(pageFoldersTitle, webFoldersPath, r),
|
||||
Folders: folders,
|
||||
}
|
||||
renderTemplate(w, templateFolders, data)
|
||||
|
|
1246
httpdtest/httpdtest.go
Normal file
|
@ -18,7 +18,6 @@ cd dist
|
|||
BASE_DIR="../.."
|
||||
|
||||
cp ${BASE_DIR}/sftpgo.json .
|
||||
cp ${BASE_DIR}/examples/rest-api-cli/sftpgo_api_cli .
|
||||
sed -i "s|sftpgo.db|/var/lib/sftpgo/sftpgo.db|" sftpgo.json
|
||||
sed -i "s|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|" sftpgo.json
|
||||
sed -i "s|\"templates\"|\"/usr/share/sftpgo/templates\"|" sftpgo.json
|
||||
|
@ -61,9 +60,6 @@ contents:
|
|||
- src: "${BASE_DIR}/init/sftpgo.service"
|
||||
dst: "/lib/systemd/system/sftpgo.service"
|
||||
|
||||
- src: "./sftpgo_api_cli"
|
||||
dst: "/usr/bin/sftpgo_api_cli"
|
||||
|
||||
- src: "${BASE_DIR}/templates/*"
|
||||
dst: "/usr/share/sftpgo/templates/"
|
||||
|
||||
|
@ -84,9 +80,6 @@ overrides:
|
|||
recommends:
|
||||
- bash-completion
|
||||
- mime-support
|
||||
suggests:
|
||||
- python3-requests
|
||||
- python3-pygments
|
||||
scripts:
|
||||
postinstall: ../scripts/deb/postinstall.sh
|
||||
preremove: ../scripts/deb/preremove.sh
|
||||
|
@ -95,7 +88,6 @@ overrides:
|
|||
recommends:
|
||||
- bash-completion
|
||||
- mailcap
|
||||
# centos 8 has python3-requests, centos 6/7 python-requests
|
||||
scripts:
|
||||
postinstall: ../scripts/rpm/postinstall
|
||||
preremove: ../scripts/rpm/preremove
|
||||
|
@ -112,6 +104,5 @@ tar xvf nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz nfpm
|
|||
chmod 755 nfpm
|
||||
mkdir rpm
|
||||
./nfpm -f nfpm.yaml pkg -p rpm -t rpm
|
||||
sed -i "s|env python|env python3|" sftpgo_api_cli
|
||||
mkdir deb
|
||||
./nfpm -f nfpm.yaml pkg -p deb -t deb
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
sftpgo usr/bin
|
||||
examples/rest-api-cli/sftpgo_api_cli usr/bin
|
||||
sftpgo.json etc/sftpgo
|
||||
init/sftpgo.service lib/systemd/system
|
||||
bash_completion/sftpgo usr/share/bash-completion/completions
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
arm64/sftpgo usr/bin
|
||||
examples/rest-api-cli/sftpgo_api_cli usr/bin
|
||||
sftpgo.json etc/sftpgo
|
||||
init/sftpgo.service lib/systemd/system
|
||||
bash_completion/sftpgo usr/share/bash-completion/completions
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
ppc64le/sftpgo usr/bin
|
||||
examples/rest-api-cli/sftpgo_api_cli usr/bin
|
||||
sftpgo.json etc/sftpgo
|
||||
init/sftpgo.service lib/systemd/system
|
||||
bash_completion/sftpgo usr/share/bash-completion/completions
|
||||
|
|
|
@ -97,7 +97,7 @@ func (s *Service) Start() error {
|
|||
|
||||
providerConf := config.GetProviderConf()
|
||||
|
||||
err = dataprovider.Initialize(providerConf, s.ConfigDir)
|
||||
err = dataprovider.Initialize(providerConf, s.ConfigDir, s.PortableMode == 0)
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "error initializing data provider: %v", err)
|
||||
logger.ErrorToConsole("error initializing data provider: %v", err)
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/httpdtest"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
@ -27,7 +27,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
|
|||
usePubKey := false
|
||||
u := getTestUserWithCryptFs(usePubKey)
|
||||
u.QuotaSize = 6553600
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -56,7 +56,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
|
|||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, encryptedFileSize, info.Size())
|
||||
}
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
@ -73,7 +73,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, err = client.Lstat(testFileName)
|
||||
assert.Error(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)
|
||||
|
@ -82,7 +82,7 @@ func TestBasicSFTPCryptoHandling(t *testing.T) {
|
|||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -93,7 +93,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) {
|
|||
usePubKey := false
|
||||
u := getTestUserWithCryptFs(usePubKey)
|
||||
u.QuotaSize = 6553600
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -113,7 +113,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -122,7 +122,7 @@ func TestOpenReadWriteCryptoFs(t *testing.T) {
|
|||
func TestEmptyFile(t *testing.T) {
|
||||
usePubKey := true
|
||||
u := getTestUserWithCryptFs(usePubKey)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -152,7 +152,7 @@ func TestEmptyFile(t *testing.T) {
|
|||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -162,7 +162,7 @@ func TestUploadResumeCryptFs(t *testing.T) {
|
|||
// upload resume is not supported
|
||||
usePubKey := true
|
||||
u := getTestUserWithCryptFs(usePubKey)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -183,7 +183,7 @@ func TestUploadResumeCryptFs(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "SSH_FX_OP_UNSUPPORTED")
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -193,7 +193,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
|
|||
usePubKey := false
|
||||
u := getTestUserWithCryptFs(usePubKey)
|
||||
u.QuotaFiles = 1000
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -213,7 +213,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
|
|||
// now replace the same file, the quota must not change
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
@ -221,7 +221,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
|
|||
// replacing a symlink is like uploading a new file
|
||||
err = client.Symlink(testFileName, testFileName+".link")
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
@ -229,14 +229,14 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
|
|||
expectedQuotaSize = expectedQuotaSize + encryptedFileSize
|
||||
err = sftpUploadFile(testFilePath, testFileName+".link", testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
}
|
||||
// now set a quota size restriction and upload the same file, upload should fail for space limit exceeded
|
||||
user.QuotaSize = encryptedFileSize*2 - 1
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client, err = getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -246,7 +246,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
|
|||
err = client.Remove(testFileName)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
@ -256,7 +256,7 @@ func TestQuotaFileReplaceCryptFs(t *testing.T) {
|
|||
|
||||
func TestQuotaScanCryptFs(t *testing.T) {
|
||||
usePubKey := false
|
||||
user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
testFileSize := int64(65535)
|
||||
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
|
||||
|
@ -274,25 +274,25 @@ func TestQuotaScanCryptFs(t *testing.T) {
|
|||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
// create user with the same home dir, so there is at least an untracked file
|
||||
user, _, err = httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||
user, _, err = httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.StartQuotaScan(user, http.StatusAccepted)
|
||||
_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool {
|
||||
scans, _, err := httpd.GetQuotaScans(http.StatusOK)
|
||||
scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
|
||||
if err == nil {
|
||||
return len(scans) == 0
|
||||
}
|
||||
return false
|
||||
}, 1*time.Second, 50*time.Millisecond)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -300,7 +300,7 @@ func TestQuotaScanCryptFs(t *testing.T) {
|
|||
|
||||
func TestGetMimeTypeCryptFs(t *testing.T) {
|
||||
usePubKey := true
|
||||
user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -325,7 +325,7 @@ func TestGetMimeTypeCryptFs(t *testing.T) {
|
|||
assert.Equal(t, "text/plain; charset=utf-8", mime)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -334,7 +334,7 @@ func TestGetMimeTypeCryptFs(t *testing.T) {
|
|||
func TestTruncate(t *testing.T) {
|
||||
// truncate is not supported
|
||||
usePubKey := true
|
||||
user, _, err := httpd.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -352,7 +352,7 @@ func TestTruncate(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -365,7 +365,7 @@ func TestSCPBasicHandlingCryptoFs(t *testing.T) {
|
|||
usePubKey := true
|
||||
u := getTestUserWithCryptFs(usePubKey)
|
||||
u.QuotaSize = 6553600
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(131074)
|
||||
|
@ -395,20 +395,20 @@ func TestSCPBasicHandlingCryptoFs(t *testing.T) {
|
|||
}
|
||||
err = os.Remove(localPath)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
// now overwrite the existing file
|
||||
err = scpUpload(testFilePath, remoteUpPath, false, false)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -422,7 +422,7 @@ func TestSCPRecursiveCryptFs(t *testing.T) {
|
|||
}
|
||||
usePubKey := true
|
||||
u := getTestUserWithCryptFs(usePubKey)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
testBaseDirName := "atestdir"
|
||||
testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)
|
||||
|
@ -467,7 +467,7 @@ func TestSCPRecursiveCryptFs(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -152,6 +152,7 @@ func (c *scpCommand) getUploadFileData(sizeToRead int64, transfer *transfer) err
|
|||
}
|
||||
|
||||
if sizeToRead > 0 {
|
||||
// we could replace this method with io.CopyN implementing "Write" method in transfer struct
|
||||
remaining := sizeToRead
|
||||
buf := make([]byte, int64(math.Min(32768, float64(sizeToRead))))
|
||||
for {
|
||||
|
@ -420,6 +421,7 @@ func (c *scpCommand) sendDownloadFileData(filePath string, stat os.FileInfo, tra
|
|||
return err
|
||||
}
|
||||
|
||||
// we could replace this method with io.CopyN implementing "Read" method in transfer struct
|
||||
buf := make([]byte, 32768)
|
||||
var n int
|
||||
for {
|
||||
|
|
1078
sftpd/sftpd_test.go
|
@ -121,7 +121,6 @@
|
|||
"sslmode": 0,
|
||||
"connection_string": "",
|
||||
"sql_tables_prefix": "",
|
||||
"manage_users": 1,
|
||||
"track_quota": 2,
|
||||
"pool_size": 0,
|
||||
"users_base_dir": "",
|
||||
|
@ -153,7 +152,6 @@
|
|||
"templates_path": "templates",
|
||||
"static_files_path": "static",
|
||||
"backups_path": "backups",
|
||||
"auth_user_file": "",
|
||||
"certificate_file": "",
|
||||
"certificate_key_file": ""
|
||||
},
|
||||
|
|
38
static/img/undraw_profile.svg
Normal 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 |
5
static/vendor/fontawesome-free/css/fontawesome.min.css
vendored
Normal file
5
static/vendor/fontawesome-free/css/solid.min.css
vendored
Normal 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}
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
2238
static/vendor/fontawesome-free/webfonts/fa-solid-900.svg
vendored
Before Width: | Height: | Size: 876 KiB After Width: | Height: | Size: 896 KiB |
92
templates/admin.html
Normal 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
|
@ -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}}
|
|
@ -15,7 +15,8 @@
|
|||
<link rel="shortcut icon" href="/static/favicon.ico" />
|
||||
|
||||
<!-- Custom fonts for this template-->
|
||||
<link href="/static/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/static/vendor/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/static/vendor/fontawesome-free/css/solid.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/static/css/fonts.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom styles for this template-->
|
||||
|
@ -38,6 +39,7 @@
|
|||
<!-- Page Wrapper -->
|
||||
<div id="wrapper">
|
||||
|
||||
{{if .LoggedAdmin.Username}}
|
||||
<!-- Sidebar -->
|
||||
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
|
||||
|
||||
|
@ -52,10 +54,10 @@
|
|||
<!-- Divider -->
|
||||
<hr class="sidebar-divider my-0">
|
||||
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "view_users"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .UsersURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.UsersURL}}">
|
||||
<i class="fas fa-fw fa-user"></i>
|
||||
<i class="fas fa-users"></i>
|
||||
<span>{{.UsersTitle}}</span></a>
|
||||
</li>
|
||||
|
||||
|
@ -64,18 +66,31 @@
|
|||
<i class="fas fa-folder"></i>
|
||||
<span>{{.FoldersTitle}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "view_conns"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.ConnectionsURL}}">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
<span>{{.ConnectionsTitle}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "manage_admins"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .AdminsURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.AdminsURL}}">
|
||||
<i class="fas fa-user-cog"></i>
|
||||
<span>{{.AdminsTitle}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "view_status"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .StatusURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.StatusURL}}">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>{{.StatusTitle}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider d-none d-md-block">
|
||||
|
@ -87,6 +102,7 @@
|
|||
|
||||
</ul>
|
||||
<!-- End of Sidebar -->
|
||||
{{end}}
|
||||
|
||||
<!-- Content Wrapper -->
|
||||
<div id="content-wrapper" class="d-flex flex-column">
|
||||
|
@ -94,11 +110,43 @@
|
|||
<!-- Main Content -->
|
||||
<div id="content">
|
||||
|
||||
{{if .LoggedAdmin.Username}}
|
||||
<!-- Topbar -->
|
||||
<nav class="mb-4 static-top">
|
||||
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
|
||||
|
||||
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<!-- Topbar Navbar -->
|
||||
<ul class="navbar-nav ml-auto">
|
||||
|
||||
<!-- Nav Item - User Information -->
|
||||
<li class="nav-item dropdown no-arrow">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mr-2 d-none d-lg-inline text-gray-600 small">{{.LoggedAdmin.Username}}</span>
|
||||
<img class="img-profile rounded-circle" src="/static/img/undraw_profile.svg">
|
||||
</a>
|
||||
<!-- Dropdown - User Information -->
|
||||
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
|
||||
<a class="dropdown-item" href="{{.ChangeAdminPwdURL}}">
|
||||
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
|
||||
Change password
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
|
||||
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
<!-- End of Topbar -->
|
||||
{{end}}
|
||||
|
||||
<!-- Begin Page Content -->
|
||||
<div class="container-fluid">
|
||||
|
@ -110,7 +158,7 @@
|
|||
|
||||
</div>
|
||||
<!-- End of Main Content -->
|
||||
|
||||
{{if .LoggedAdmin.Username}}
|
||||
<!-- Footer -->
|
||||
<footer class="sticky-footer bg-white">
|
||||
<div class="container my-auto">
|
||||
|
@ -120,6 +168,7 @@
|
|||
</div>
|
||||
</footer>
|
||||
<!-- End of Footer -->
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
<!-- End of Content Wrapper -->
|
||||
|
@ -132,6 +181,26 @@
|
|||
<i class="fas fa-angle-up"></i>
|
||||
</a>
|
||||
|
||||
<!-- Logout Modal-->
|
||||
<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalLabel">Ready to Leave?</h5>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
|
||||
<a class="btn btn-primary" href="{{.LogoutURL}}">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{block "dialog" .}}{{end}}
|
||||
|
||||
<!-- Bootstrap core JavaScript-->
|
||||
|
|
38
templates/changepwd.html
Normal file
|
@ -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}}
|
|
@ -90,9 +90,9 @@
|
|||
|
||||
function disconnectAction() {
|
||||
var table = $('#dataTable').DataTable();
|
||||
table.button(0).enable(false);
|
||||
table.button('disconnect:name').enable(false);
|
||||
var connectionID = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.APIConnectionsURL}}' + "/" + connectionID;
|
||||
var path = '{{.ConnectionsURL}}' + "/" + connectionID;
|
||||
$('#disconnectModal').modal('hide');
|
||||
$.ajax({
|
||||
url: path,
|
||||
|
@ -101,12 +101,12 @@
|
|||
timeout: 15000,
|
||||
success: function (result) {
|
||||
setTimeout(function () {
|
||||
table.button(0).enable(true);
|
||||
table.button('disconnect:name').enable(true);
|
||||
window.location.href = '{{.ConnectionsURL}}';
|
||||
}, 1000);
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
table.button(0).enable(true);
|
||||
table.button('disconnect:name').enable(true);
|
||||
var txt = "Unable to close the selected connection";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
|
@ -126,6 +126,7 @@
|
|||
$(document).ready(function () {
|
||||
$.fn.dataTable.ext.buttons.disconnect = {
|
||||
text: 'Disconnect',
|
||||
name: 'disconnect',
|
||||
action: function (e, dt, node, config) {
|
||||
$('#disconnectModal').modal('show');
|
||||
},
|
||||
|
@ -138,9 +139,7 @@
|
|||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
select: true,
|
||||
buttons: [
|
||||
'disconnect'
|
||||
],
|
||||
buttons: [],
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
|
@ -152,10 +151,14 @@
|
|||
"order": [[1, 'asc']]
|
||||
});
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "close_conns"}}
|
||||
table.button().add(0,'disconnect');
|
||||
|
||||
table.on('select deselect', function () {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
table.button(0).enable(selectedRows == 1);
|
||||
table.button('disconnect:name').enable(selectedRows == 1);
|
||||
});
|
||||
{{end}}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
|
@ -87,9 +87,9 @@
|
|||
|
||||
function deleteAction() {
|
||||
var table = $('#dataTable').DataTable();
|
||||
table.button(1).enable(false);
|
||||
table.button('delete:name').enable(false);
|
||||
var folderPath = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.APIFoldersURL}}' + "?folder_path=" + encodeURIComponent(folderPath);
|
||||
var path = '{{.FolderURL}}' + "?folder_path=" + encodeURIComponent(folderPath);
|
||||
$('#deleteModal').modal('hide');
|
||||
$.ajax({
|
||||
url: path,
|
||||
|
@ -97,12 +97,11 @@ function deleteAction() {
|
|||
dataType: 'json',
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
table.button(1).enable(true);
|
||||
table.button('delete:name').enable(true);
|
||||
window.location.href = '{{.FoldersURL}}';
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
console.log("delete error")
|
||||
table.button(1).enable(true);
|
||||
table.button('delete:name').enable(true);
|
||||
var txt = "Unable to delete the selected folder";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
|
@ -122,6 +121,7 @@ function deleteAction() {
|
|||
$(document).ready(function () {
|
||||
$.fn.dataTable.ext.buttons.add = {
|
||||
text: 'Add',
|
||||
name: 'add',
|
||||
action: function (e, dt, node, config) {
|
||||
window.location.href = '{{.FolderURL}}';
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ function deleteAction() {
|
|||
|
||||
$.fn.dataTable.ext.buttons.delete = {
|
||||
text: 'Delete',
|
||||
name: 'delete',
|
||||
action: function (e, dt, node, config) {
|
||||
$('#deleteModal').modal('show');
|
||||
},
|
||||
|
@ -137,10 +138,11 @@ function deleteAction() {
|
|||
|
||||
$.fn.dataTable.ext.buttons.quota_scan = {
|
||||
text: 'Quota scan',
|
||||
name: 'quota_scan',
|
||||
action: function (e, dt, node, config) {
|
||||
table.button(2).enable(false);
|
||||
dt.button('quota_scan:name').enable(false);
|
||||
var folderPath = dt.row({ selected: true }).data()[0];
|
||||
var path = '{{.APIFolderQuotaScanURL}}'
|
||||
var path = '{{.FolderQuotaScanURL}}'
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
|
@ -148,7 +150,7 @@ function deleteAction() {
|
|||
data: JSON.stringify({ "mapped_path": folderPath }),
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
table.button(2).enable(true);
|
||||
dt.button('quota_scan:name').enable(true);
|
||||
$('#successTxt').text("Quota scan started for the selected folder. Please reload the folders page to check when the scan ends");
|
||||
$('#successMsg').show();
|
||||
setTimeout(function () {
|
||||
|
@ -156,8 +158,7 @@ function deleteAction() {
|
|||
}, 5000);
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
console.log("quota scan error")
|
||||
table.button(2).enable(true);
|
||||
dt.button('quota_scan:name').enable(true);
|
||||
var txt = "Unable to update quota for the selected folder";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
|
@ -186,17 +187,31 @@ function deleteAction() {
|
|||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
select: true,
|
||||
buttons: [
|
||||
'add','delete', 'quota_scan'
|
||||
],
|
||||
buttons: [],
|
||||
"scrollX": false,
|
||||
"order": [[0, 'asc']]
|
||||
});
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "quota_scans"}}
|
||||
table.button().add(0,'quota_scan');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "del_users"}}
|
||||
table.button().add(0,'delete');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "add_users"}}
|
||||
table.button().add(0,'add');
|
||||
{{end}}
|
||||
|
||||
table.on('select deselect', function () {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
table.button(1).enable(selectedRows == 1);
|
||||
table.button(2).enable(selectedRows == 1);
|
||||
{{if .LoggedAdmin.HasPermission "del_users"}}
|
||||
table.button('delete:name').enable(selectedRows == 1);
|
||||
{{end}}
|
||||
{{if .LoggedAdmin.HasPermission "quota_scans"}}
|
||||
table.button('quota_scan:name').enable(selectedRows == 1);
|
||||
{{end}}
|
||||
});
|
||||
|
||||
});
|
||||
|
|
109
templates/login.html
Normal file
|
@ -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>
|
|
@ -3,6 +3,7 @@
|
|||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
{{if .LoggedAdmin.Username}}
|
||||
<h1 class="h5 mb-4 text-gray-800">{{.Title}}</h1>
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
|
@ -15,5 +16,36 @@
|
|||
<div class="card-body">{{.Success}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col-xl-8 col-lg-9 col-md-9">
|
||||
|
||||
<div class="card o-hidden border-0 shadow-lg my-5">
|
||||
<div class="card-body p-0">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-9">
|
||||
<div class="p-5">
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-4">{{.Title}}</h1>
|
||||
</div>
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Success}}
|
||||
<div class="card mb-4 border-left-success">
|
||||
<div class="card-body">{{.Success}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
|
@ -50,7 +50,7 @@
|
|||
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idPassword" name="password" placeholder=""
|
||||
autocomplete="new-password" {{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
|
||||
{{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
|
||||
{{if not .IsAdd}}
|
||||
<small id="pwdHelpBlock" class="form-text text-muted">
|
||||
If empty the current password will not be changed
|
||||
|
|
|
@ -97,22 +97,21 @@
|
|||
|
||||
function deleteAction() {
|
||||
var table = $('#dataTable').DataTable();
|
||||
table.button(3).enable(false);
|
||||
var userID = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.APIUserURL}}' + "/" + userID;
|
||||
table.button('delete:name').enable(false);
|
||||
var username = table.row({ selected: true }).data()[1];
|
||||
var path = '{{.UserURL}}' + "/" + username;
|
||||
$('#deleteModal').modal('hide');
|
||||
$.ajax({
|
||||
url: path,
|
||||
url: encodeURI(path),
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
table.button(3).enable(true);
|
||||
table.button('delete:name').enable(true);
|
||||
window.location.href = '{{.UsersURL}}';
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
console.log("delete error")
|
||||
table.button(3).enable(true);
|
||||
table.button('delete:name').enable(true);
|
||||
var txt = "Unable to delete the selected user";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
|
@ -132,6 +131,7 @@
|
|||
$(document).ready(function () {
|
||||
$.fn.dataTable.ext.buttons.add = {
|
||||
text: 'Add',
|
||||
name: 'add',
|
||||
action: function (e, dt, node, config) {
|
||||
window.location.href = '{{.UserURL}}';
|
||||
}
|
||||
|
@ -139,19 +139,21 @@
|
|||
|
||||
$.fn.dataTable.ext.buttons.edit = {
|
||||
text: 'Edit',
|
||||
name: 'edit',
|
||||
action: function (e, dt, node, config) {
|
||||
var userID = dt.row({ selected: true }).data()[0];
|
||||
var path = '{{.UserURL}}' + "/" + userID;
|
||||
window.location.href = path;
|
||||
var username = dt.row({ selected: true }).data()[1];
|
||||
var path = '{{.UserURL}}' + "/" + username;
|
||||
window.location.href = encodeURI(path);
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.clone = {
|
||||
text: 'Clone',
|
||||
name: 'clone',
|
||||
action: function (e, dt, node, config) {
|
||||
var userID = dt.row({ selected: true }).data()[0];
|
||||
var path = '{{.UserURL}}' + "?cloneFromId=" + userID;
|
||||
var username = dt.row({ selected: true }).data()[1];
|
||||
var path = '{{.UserURL}}' + "?cloneFrom=" + encodeURIComponent(username);
|
||||
window.location.href = path;
|
||||
},
|
||||
enabled: false
|
||||
|
@ -159,6 +161,7 @@
|
|||
|
||||
$.fn.dataTable.ext.buttons.delete = {
|
||||
text: 'Delete',
|
||||
name: 'delete',
|
||||
action: function (e, dt, node, config) {
|
||||
/*console.log("delete clicked, num row selected: " + dt.rows({ selected: true }).count());
|
||||
var data = dt.rows({ selected: true }).data();
|
||||
|
@ -172,10 +175,11 @@
|
|||
|
||||
$.fn.dataTable.ext.buttons.quota_scan = {
|
||||
text: 'Quota scan',
|
||||
name: 'quota_scan',
|
||||
action: function (e, dt, node, config) {
|
||||
table.button(4).enable(false);
|
||||
dt.button('quota_scan:name').enable(false);
|
||||
var username = dt.row({ selected: true }).data()[1];
|
||||
var path = '{{.APIQuotaScanURL}}'
|
||||
var path = '{{.QuotaScanURL}}'
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
|
@ -183,7 +187,7 @@
|
|||
data: JSON.stringify({ "username": username }),
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
table.button(4).enable(true);
|
||||
dt.button('quota_scan:name').enable(true);
|
||||
$('#successTxt').text("Quota scan started for the selected user. Please reload the user's page to check when the scan ends");
|
||||
$('#successMsg').show();
|
||||
setTimeout(function () {
|
||||
|
@ -191,8 +195,7 @@
|
|||
}, 5000);
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
console.log("quota scan error")
|
||||
table.button(4).enable(true);
|
||||
dt.button('quota_scan:name').enable(true);
|
||||
var txt = "Unable to update quota for the selected user";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
|
@ -221,9 +224,7 @@
|
|||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
select: true,
|
||||
buttons: [
|
||||
'add', 'edit', 'clone', 'delete', 'quota_scan'
|
||||
],
|
||||
buttons: [],
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
|
@ -235,12 +236,41 @@
|
|||
"order": [[1, 'asc']]
|
||||
});
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "quota_scans"}}
|
||||
table.button().add(0,'quota_scan');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "del_users"}}
|
||||
table.button().add(0,'delete');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "add_users"}}
|
||||
table.button().add(0,'clone');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "edit_users"}}
|
||||
table.button().add(0,'edit');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "add_users"}}
|
||||
table.button().add(0,'add');
|
||||
{{end}}
|
||||
|
||||
|
||||
table.on('select deselect', function () {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
table.button(1).enable(selectedRows == 1);
|
||||
table.button(2).enable(selectedRows == 1);
|
||||
table.button(3).enable(selectedRows == 1);
|
||||
table.button(4).enable(selectedRows == 1);
|
||||
{{if .LoggedAdmin.HasPermission "edit_users"}}
|
||||
table.button('edit:name').enable(selectedRows == 1);
|
||||
{{end}}
|
||||
{{if .LoggedAdmin.HasPermission "add_users"}}
|
||||
table.button('clone:name').enable(selectedRows == 1);
|
||||
{{end}}
|
||||
{{if .LoggedAdmin.HasPermission "del_users"}}
|
||||
table.button('delete:name').enable(selectedRows == 1);
|
||||
{{end}}
|
||||
{{if .LoggedAdmin.HasPermission "quota_scans"}}
|
||||
table.button('quota_scan:name').enable(selectedRows == 1);
|
||||
{{end}}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -26,12 +26,15 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
)
|
||||
|
||||
const logSender = "utils"
|
||||
const (
|
||||
logSender = "utils"
|
||||
)
|
||||
|
||||
// IsStringInSlice searches a string in a slice and returns true if the string is found
|
||||
func IsStringInSlice(obj string, list []string) bool {
|
||||
|
@ -383,6 +386,22 @@ func createDirPathIfMissing(file string, perm os.FileMode) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GenerateRandomBytes generates the secret to use for JWT auth
|
||||
func GenerateRandomBytes(length int) []byte {
|
||||
b := make([]byte, length)
|
||||
_, err := io.ReadFull(rand.Reader, b)
|
||||
if err != nil {
|
||||
return b
|
||||
}
|
||||
|
||||
b = xid.New().Bytes()
|
||||
for len(b) < length {
|
||||
b = append(b, xid.New().Bytes()...)
|
||||
}
|
||||
|
||||
return b[:length]
|
||||
}
|
||||
|
||||
// HTTPListenAndServe is a wrapper for ListenAndServe that support both tcp
|
||||
// and Unix-domain sockets
|
||||
func HTTPListenAndServe(srv *http.Server, address string, port int, isTLS bool, logSender string) error {
|
||||
|
|
|
@ -980,7 +980,7 @@ func TestBasicUsersCache(t *testing.T) {
|
|||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||
assert.True(t, ok)
|
||||
// cache is invalidated after user deletion
|
||||
err = dataprovider.DeleteUser(&user)
|
||||
err = dataprovider.DeleteUser(user.Username)
|
||||
assert.NoError(t, err)
|
||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||
assert.False(t, ok)
|
||||
|
@ -1164,13 +1164,13 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
|||
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
|
||||
assert.True(t, ok)
|
||||
|
||||
err = dataprovider.DeleteUser(&user1)
|
||||
err = dataprovider.DeleteUser(user1.Username)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.DeleteUser(&user2)
|
||||
err = dataprovider.DeleteUser(user2.Username)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.DeleteUser(&user3)
|
||||
err = dataprovider.DeleteUser(user3.Username)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.DeleteUser(&user4)
|
||||
err = dataprovider.DeleteUser(user4.Username)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.RemoveAll(u.GetHomeDir())
|
||||
|
|
|
@ -29,7 +29,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpclient"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/httpdtest"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
|
@ -127,7 +127,7 @@ func TestMain(m *testing.M) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing data provider: %v", err)
|
||||
os.Exit(1)
|
||||
|
@ -144,7 +144,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
httpdConf.BindPort = 8078
|
||||
httpd.SetBaseURLAndCredentials("http://127.0.0.1:8078", "", "")
|
||||
httpdtest.SetBaseURL("http://127.0.0.1:8078")
|
||||
|
||||
// required to test sftpfs
|
||||
sftpdConf := config.GetSFTPDConfig()
|
||||
|
@ -288,11 +288,11 @@ func TestInitialization(t *testing.T) {
|
|||
func TestBasicHandling(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.QuotaSize = 6553600
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.QuotaSize = 6553600
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
client := getWebDavClient(user)
|
||||
|
@ -311,7 +311,7 @@ func TestBasicHandling(t *testing.T) {
|
|||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
@ -322,13 +322,13 @@ func TestBasicHandling(t *testing.T) {
|
|||
// the webdav client hide the error we check the quota
|
||||
err = client.Remove(testFileName)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
err = client.Remove(testFileName + "1")
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
||||
|
@ -364,9 +364,9 @@ func TestBasicHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -378,7 +378,7 @@ func TestBasicHandling(t *testing.T) {
|
|||
func TestBasicHandlingCryptFs(t *testing.T) {
|
||||
u := getTestUserWithCryptFs()
|
||||
u.QuotaSize = 6553600
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
@ -399,7 +399,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
|||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
|
@ -410,7 +410,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
|||
}
|
||||
err = client.Remove(testFileName)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)
|
||||
|
@ -443,7 +443,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -453,13 +453,13 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
|||
func TestPropPatch(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Username = u.Username + "1"
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser := getTestSFTPUser()
|
||||
sftpUser.FsConfig.SFTPConfig.Username = localUser.Username
|
||||
|
||||
for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} {
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client), sftpUser.Username)
|
||||
|
@ -486,13 +486,13 @@ func TestPropPatch(t *testing.T) {
|
|||
}
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
}
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -500,14 +500,14 @@ func TestPropPatch(t *testing.T) {
|
|||
|
||||
func TestLoginInvalidPwd(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
user.Password = "wrong"
|
||||
client = getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -527,7 +527,7 @@ func TestDefender(t *testing.T) {
|
|||
err := common.Initialize(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
@ -545,7 +545,7 @@ func TestDefender(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "403")
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -556,26 +556,26 @@ func TestDefender(t *testing.T) {
|
|||
|
||||
func TestLoginInvalidURL(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u1 := getTestUser()
|
||||
u1.Username = user.Username + "1"
|
||||
user1, _, err := httpd.AddUser(u1, http.StatusOK)
|
||||
user1, _, err := httpdtest.AddUser(u1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
rootPath := fmt.Sprintf("http://%v/%v", webDavServerAddr, user.Username+"1")
|
||||
client := gowebdav.NewClient(rootPath, user.Username, defaultPassword)
|
||||
client.SetTimeout(5 * time.Second)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user1, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRootRedirect(t *testing.T) {
|
||||
errRedirect := errors.New("redirect error")
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
@ -612,7 +612,7 @@ func TestRootRedirect(t *testing.T) {
|
|||
err = resp.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -630,29 +630,26 @@ func TestLoginExternalAuth(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
providerConf.ExternalAuthHook = extAuthPath
|
||||
providerConf.ExternalAuthScope = 0
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(u)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
u.Username = defaultUsername + "1"
|
||||
client = getWebDavClient(u)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, users, 1) {
|
||||
user := users[0]
|
||||
assert.Equal(t, defaultUsername, user.Username)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
err = dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf = config.GetProviderConf()
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(extAuthPath)
|
||||
assert.NoError(t, err)
|
||||
|
@ -671,30 +668,27 @@ func TestPreLoginHook(t *testing.T) {
|
|||
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
providerConf.PreLoginHook = preLoginPath
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||
_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(users))
|
||||
client := getWebDavClient(u)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
||||
users, _, err = httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
user := users[0]
|
||||
// test login with an existing user
|
||||
client = getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
// update the user to remove it from the cache
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client = getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
// update the user to remove it from the cache
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
user.Status = 0
|
||||
err = ioutil.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)
|
||||
|
@ -702,7 +696,7 @@ func TestPreLoginHook(t *testing.T) {
|
|||
client = getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -711,7 +705,7 @@ func TestPreLoginHook(t *testing.T) {
|
|||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf = config.GetProviderConf()
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(preLoginPath)
|
||||
assert.NoError(t, err)
|
||||
|
@ -724,7 +718,7 @@ func TestPostConnectHook(t *testing.T) {
|
|||
common.Config.PostConnectHook = postConnectPath
|
||||
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
err = ioutil.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
@ -734,13 +728,13 @@ func TestPostConnectHook(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
|
||||
common.Config.PostConnectHook = "http://127.0.0.1:8078/api/v1/version"
|
||||
common.Config.PostConnectHook = "http://127.0.0.1:8078/healthz"
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
||||
common.Config.PostConnectHook = "http://127.0.0.1:8078/notfound"
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -752,7 +746,7 @@ func TestMaxConnections(t *testing.T) {
|
|||
oldValue := common.Config.MaxTotalConnections
|
||||
common.Config.MaxTotalConnections = 1
|
||||
|
||||
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
@ -764,7 +758,7 @@ func TestMaxConnections(t *testing.T) {
|
|||
common.Connections.Add(connection)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
common.Connections.Remove(connection.GetID())
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -776,7 +770,7 @@ func TestMaxConnections(t *testing.T) {
|
|||
func TestMaxSessions(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.MaxSessions = 1
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
@ -788,7 +782,7 @@ func TestMaxSessions(t *testing.T) {
|
|||
common.Connections.Add(connection)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
common.Connections.Remove(connection.GetID())
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -799,12 +793,12 @@ func TestLoginWithIPilters(t *testing.T) {
|
|||
u := getTestUser()
|
||||
u.Filters.DeniedIP = []string{"192.167.0.0/24", "172.18.0.0/16"}
|
||||
u.Filters.AllowedIP = []string{"172.19.0.0/16"}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -833,7 +827,7 @@ func TestDownloadErrors(t *testing.T) {
|
|||
DeniedPatterns: []string{"*.jpg"},
|
||||
},
|
||||
}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
testFilePath1 := filepath.Join(user.HomeDir, subDir1, "file.zipp")
|
||||
|
@ -861,7 +855,7 @@ func TestDownloadErrors(t *testing.T) {
|
|||
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -883,7 +877,7 @@ func TestUploadErrors(t *testing.T) {
|
|||
DeniedExtensions: []string{".zip"},
|
||||
},
|
||||
}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
|
@ -918,7 +912,7 @@ func TestUploadErrors(t *testing.T) {
|
|||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -927,18 +921,18 @@ func TestUploadErrors(t *testing.T) {
|
|||
func TestDeniedLoginMethod(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
|
||||
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndKeyboardInt}
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client = getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -947,18 +941,18 @@ func TestDeniedLoginMethod(t *testing.T) {
|
|||
func TestDeniedProtocols(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
|
||||
user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolFTP}
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
client = getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -967,11 +961,11 @@ func TestDeniedProtocols(t *testing.T) {
|
|||
func TestQuotaLimits(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.QuotaFiles = 1
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.QuotaFiles = 1
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
testFileSize := int64(65535)
|
||||
|
@ -1002,7 +996,7 @@ func TestQuotaLimits(t *testing.T) {
|
|||
// test quota size
|
||||
user.QuotaSize = testFileSize - 1
|
||||
user.QuotaFiles = 0
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
err = uploadFile(testFilePath, testFileName+".quota", testFileSize, client)
|
||||
assert.Error(t, err)
|
||||
|
@ -1011,7 +1005,7 @@ func TestQuotaLimits(t *testing.T) {
|
|||
// now test quota limits while uploading the current file, we have 1 bytes remaining
|
||||
user.QuotaSize = testFileSize + 1
|
||||
user.QuotaFiles = 0
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
err = uploadFile(testFilePath1, testFileName1, testFileSize1, client)
|
||||
assert.Error(t, err)
|
||||
|
@ -1040,13 +1034,13 @@ func TestQuotaLimits(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
user.QuotaFiles = 0
|
||||
user.QuotaSize = 0
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1056,11 +1050,11 @@ func TestUploadMaxSize(t *testing.T) {
|
|||
testFileSize := int64(65535)
|
||||
u := getTestUser()
|
||||
u.Filters.MaxUploadFileSize = testFileSize + 1
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.Filters.MaxUploadFileSize = testFileSize + 1
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
|
@ -1090,13 +1084,13 @@ func TestUploadMaxSize(t *testing.T) {
|
|||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
user.Filters.MaxUploadFileSize = 65536000
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1106,12 +1100,12 @@ func TestClientClose(t *testing.T) {
|
|||
u := getTestUser()
|
||||
u.UploadBandwidth = 64
|
||||
u.DownloadBandwidth = 64
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.UploadBandwidth = 64
|
||||
u.DownloadBandwidth = 64
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
testFileSize := int64(1048576)
|
||||
|
@ -1179,9 +1173,9 @@ func TestClientClose(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1202,7 +1196,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
|
||||
assert.NoError(t, dataprovider.Close())
|
||||
|
||||
err := dataprovider.Initialize(providerConf, configDir)
|
||||
err := dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if _, err = os.Stat(credentialsFile); err == nil {
|
||||
|
@ -1210,7 +1204,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
assert.NoError(t, os.Remove(credentialsFile))
|
||||
}
|
||||
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
|
@ -1224,7 +1218,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
err = client.Connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1232,7 +1226,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
assert.NoError(t, dataprovider.Close())
|
||||
assert.NoError(t, config.LoadConfig(configDir, ""))
|
||||
providerConf = config.GetProviderConf()
|
||||
assert.NoError(t, dataprovider.Initialize(providerConf, configDir))
|
||||
assert.NoError(t, dataprovider.Initialize(providerConf, configDir, true))
|
||||
}
|
||||
|
||||
func TestLoginInvalidFs(t *testing.T) {
|
||||
|
@ -1240,7 +1234,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "test"
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
providerConf := config.GetProviderConf()
|
||||
|
@ -1256,7 +1250,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
client := getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1265,13 +1259,13 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
func TestBytesRangeRequests(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Username = u.Username + "1"
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
sftpUser := getTestSFTPUser()
|
||||
sftpUser.FsConfig.SFTPConfig.Username = localUser.Username
|
||||
|
||||
for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} {
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
testFileName := "test_file.txt"
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
|
@ -1309,12 +1303,12 @@ func TestBytesRangeRequests(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1324,7 +1318,7 @@ func TestGETAsPROPFIND(t *testing.T) {
|
|||
u := getTestUser()
|
||||
subDir1 := "/sub1"
|
||||
u.Permissions[subDir1] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
rootPath := fmt.Sprintf("http://%v/%v", webDavServerAddr, user.Username)
|
||||
httpClient := httpclient.GetHTTPClient()
|
||||
|
@ -1371,13 +1365,13 @@ func TestGETAsPROPFIND(t *testing.T) {
|
|||
assert.Len(t, files, 0)
|
||||
// if we grant the permissions the files are listed
|
||||
user.Permissions[subDir1] = []string{dataprovider.PermDownload, dataprovider.PermListItems}
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
files, err = client.ReadDir(subDir1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, files, 1)
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1386,7 +1380,7 @@ func TestGETAsPROPFIND(t *testing.T) {
|
|||
func TestStat(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermDownload}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
subDir := "subdir"
|
||||
|
@ -1401,7 +1395,7 @@ func TestStat(t *testing.T) {
|
|||
err = uploadFile(testFilePath, path.Join("/", subDir, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermDownload}
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.NoError(t, err)
|
||||
|
@ -1410,7 +1404,7 @@ func TestStat(t *testing.T) {
|
|||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1430,7 +1424,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
})
|
||||
err := os.MkdirAll(mappedPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
files, err := client.ReadDir(".")
|
||||
|
@ -1454,7 +1448,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = uploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
folder, _, err := httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
folder, _, err := httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
|
@ -1463,7 +1457,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
}
|
||||
err = uploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
folder, _, err = httpd.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
folder, _, err = httpdtest.GetFolders(0, 0, mappedPath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folder, 1) {
|
||||
f := folder[0]
|
||||
|
@ -1472,9 +1466,9 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
}
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
|
||||
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
@ -1485,11 +1479,11 @@ func TestUploadOverwriteVfolder(t *testing.T) {
|
|||
func TestMiscCommands(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.QuotaFiles = 100
|
||||
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.QuotaFiles = 100
|
||||
sftpUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
dir := "testDir"
|
||||
|
@ -1508,7 +1502,7 @@ func TestMiscCommands(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = client.Copy(dir, dir+"_copy", false)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 6*testFileSize, user.UsedQuotaSize)
|
||||
|
@ -1518,7 +1512,7 @@ func TestMiscCommands(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
err = client.Copy(dir+"_copy", dir+"_copy1", true)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 9, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 9*testFileSize, user.UsedQuotaSize)
|
||||
|
@ -1532,7 +1526,7 @@ func TestMiscCommands(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = client.RemoveAll(dir + "_copy1")
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, user.UsedQuotaFiles)
|
||||
assert.Equal(t, 6*testFileSize, user.UsedQuotaSize)
|
||||
|
@ -1543,13 +1537,13 @@ func TestMiscCommands(t *testing.T) {
|
|||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
user.QuotaFiles = 0
|
||||
_, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -44,7 +44,6 @@ Source: "{#MyAppDir}\sftpgo.exe"; DestDir: "{app}"; Flags: ignoreversion
|
|||
Source: "{#MyAppDir}\sftpgo.db"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall
|
||||
Source: "{#MyAppDir}\LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#MyAppDir}\sftpgo.json"; DestDir: "{commonappdata}\{#MyAppName}"; Flags: onlyifdoesntexist uninsneveruninstall
|
||||
Source: "{#MyAppDir}\sftpgo_api_cli.exe"; DestDir: "{app}\examples\rest-api-cli"; Flags: ignoreversion; MinVersion: 10
|
||||
Source: "{#MyAppDir}\templates\*"; DestDir: "{commonappdata}\{#MyAppName}\templates"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#MyAppDir}\static\*"; DestDir: "{commonappdata}\{#MyAppName}\static"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
|
@ -56,7 +55,6 @@ Name: "{commonappdata}\{#MyAppName}\credentials"; Permissions: everyone-full
|
|||
[Icons]
|
||||
Name: "{group}\Web Admin"; Filename: "http://127.0.0.1:8080/web";
|
||||
Name: "{group}\Service Control"; WorkingDir: "{app}"; Filename: "powershell.exe"; Parameters: "-Command ""Start-Process cmd \""/k cd {app} & {#MyAppExeName} service --help\"" -Verb RunAs"; Comment: "Manage SFTPGo Service"
|
||||
Name: "{group}\REST API CLI"; WorkingDir: "{app}\examples\rest-api-cli"; Filename: "{cmd}"; Parameters: "/k sftpgo_api_cli.exe --help"; Comment: "Manage users, folders and connections"; MinVersion: 10
|
||||
Name: "{group}\Documentation"; Filename: "{#DocURL}";
|
||||
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
|
||||
|
||||
|
|