mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
allow to cache external authentications
Fixes #733 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
f5a0559be6
commit
4e9dae6fa4
13 changed files with 182 additions and 22 deletions
6
.github/workflows/development.yml
vendored
6
.github/workflows/development.yml
vendored
|
@ -448,8 +448,12 @@ jobs:
|
|||
name: golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
|
|
|
@ -1122,7 +1122,11 @@ func UpdateAPIKeyLastUse(apiKey *APIKey) error {
|
|||
|
||||
// UpdateLastLogin updates the last login field for the given SFTPGo user
|
||||
func UpdateLastLogin(user *User) {
|
||||
if !isLastActivityRecent(user.LastLogin) {
|
||||
delay := lastLoginMinDelay
|
||||
if user.Filters.ExternalAuthCacheTime > 0 {
|
||||
delay = time.Duration(user.Filters.ExternalAuthCacheTime) * time.Second
|
||||
}
|
||||
if !isLastActivityRecent(user.LastLogin, delay) {
|
||||
err := provider.updateLastLogin(user.Username)
|
||||
if err == nil {
|
||||
webDAVUsersCache.updateLastLogin(user.Username)
|
||||
|
@ -1132,7 +1136,7 @@ func UpdateLastLogin(user *User) {
|
|||
|
||||
// UpdateAdminLastLogin updates the last login field for the given SFTPGo admin
|
||||
func UpdateAdminLastLogin(admin *Admin) {
|
||||
if !isLastActivityRecent(admin.LastLogin) {
|
||||
if !isLastActivityRecent(admin.LastLogin, lastLoginMinDelay) {
|
||||
provider.updateAdminLastLogin(admin.Username) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
@ -2052,6 +2056,9 @@ func validateFilters(user *User) error {
|
|||
return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
|
||||
}
|
||||
}
|
||||
if !user.HasExternalAuth() {
|
||||
user.Filters.ExternalAuthCacheTime = 0
|
||||
}
|
||||
|
||||
return validateFiltersPatternExtensions(user)
|
||||
}
|
||||
|
@ -3207,7 +3214,9 @@ func updateUserFromExtAuthResponse(user *User, password, pkey string) {
|
|||
}
|
||||
}
|
||||
|
||||
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
|
||||
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string,
|
||||
tlsCert *x509.Certificate,
|
||||
) (User, error) {
|
||||
var user User
|
||||
|
||||
u, userAsJSON, err := getUserAndJSONForHook(username)
|
||||
|
@ -3219,6 +3228,10 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
|||
return u, nil
|
||||
}
|
||||
|
||||
if u.isExternalAuthCached() {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
pkey, err := util.GetSSHPublicKeyAsString(pubKey)
|
||||
if err != nil {
|
||||
return user, err
|
||||
|
@ -3295,6 +3308,10 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
|||
return u, nil
|
||||
}
|
||||
|
||||
if u.isExternalAuthCached() {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
pkey, err := util.GetSSHPublicKeyAsString(pubKey)
|
||||
if err != nil {
|
||||
return user, err
|
||||
|
@ -3368,15 +3385,15 @@ func getUserAndJSONForHook(username string) (User, []byte, error) {
|
|||
return u, userAsJSON, err
|
||||
}
|
||||
|
||||
func providerLog(level logger.LogLevel, format string, v ...interface{}) {
|
||||
logger.Log(level, logSender, "", format, v...)
|
||||
}
|
||||
|
||||
func isLastActivityRecent(lastActivity int64) bool {
|
||||
func isLastActivityRecent(lastActivity int64, minDelay time.Duration) bool {
|
||||
lastActivityTime := util.GetTimeFromMsecSinceEpoch(lastActivity)
|
||||
diff := -time.Until(lastActivityTime)
|
||||
if diff < -10*time.Second {
|
||||
return false
|
||||
}
|
||||
return diff < lastLoginMinDelay
|
||||
return diff < minDelay
|
||||
}
|
||||
|
||||
func providerLog(level logger.LogLevel, format string, v ...interface{}) {
|
||||
logger.Log(level, logSender, "", format, v...)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/mfa"
|
||||
"github.com/drakkan/sftpgo/v2/plugin"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
@ -203,7 +204,14 @@ func (u *User) CheckFsRoot(connectionID string) error {
|
|||
if u.Filters.DisableFsChecks {
|
||||
return nil
|
||||
}
|
||||
if isLastActivityRecent(u.LastLogin) {
|
||||
delay := lastLoginMinDelay
|
||||
if u.Filters.ExternalAuthCacheTime > 0 {
|
||||
cacheTime := time.Duration(u.Filters.ExternalAuthCacheTime) * time.Second
|
||||
if cacheTime > delay {
|
||||
delay = cacheTime
|
||||
}
|
||||
}
|
||||
if isLastActivityRecent(u.LastLogin, delay) {
|
||||
return nil
|
||||
}
|
||||
fs, err := u.GetFilesystemForPath("/", connectionID)
|
||||
|
@ -924,6 +932,17 @@ func (u *User) CanManageMFA() bool {
|
|||
return len(mfa.GetAvailableTOTPConfigs()) > 0
|
||||
}
|
||||
|
||||
func (u *User) isExternalAuthCached() bool {
|
||||
if u.ID <= 0 {
|
||||
return false
|
||||
}
|
||||
if u.Filters.ExternalAuthCacheTime <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return isLastActivityRecent(u.LastLogin, time.Duration(u.Filters.ExternalAuthCacheTime)*time.Second)
|
||||
}
|
||||
|
||||
// CanManageShares returns true if the user can add, update and list shares
|
||||
func (u *User) CanManageShares() bool {
|
||||
return !util.IsStringInSlice(sdk.WebClientSharesDisabled, u.Filters.WebClient)
|
||||
|
@ -1106,7 +1125,7 @@ func (u *User) GetHomeDir() string {
|
|||
|
||||
// HasRecentActivity returns true if the last user login is recent and so we can skip some expensive checks
|
||||
func (u *User) HasRecentActivity() bool {
|
||||
return isLastActivityRecent(u.LastLogin)
|
||||
return isLastActivityRecent(u.LastLogin, lastLoginMinDelay)
|
||||
}
|
||||
|
||||
// HasQuotaRestrictions returns true if there are any disk quota restrictions
|
||||
|
@ -1310,6 +1329,18 @@ func (u *User) GetDeniedIPAsString() string {
|
|||
return strings.Join(u.Filters.DeniedIP, ",")
|
||||
}
|
||||
|
||||
// HasExternalAuth returns true if the external authentication is globally enabled
|
||||
// and it is not disabled for this user
|
||||
func (u *User) HasExternalAuth() bool {
|
||||
if u.Filters.Hooks.ExternalAuthDisabled {
|
||||
return false
|
||||
}
|
||||
if config.ExternalAuthHook != "" {
|
||||
return true
|
||||
}
|
||||
return plugin.Handler.HasAuthenticators()
|
||||
}
|
||||
|
||||
// CountUnusedRecoveryCodes returns the number of unused recovery codes
|
||||
func (u *User) CountUnusedRecoveryCodes() int {
|
||||
unused := 0
|
||||
|
@ -1372,6 +1403,7 @@ func (u *User) getACopy() User {
|
|||
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
|
||||
filters.DisableFsChecks = u.Filters.DisableFsChecks
|
||||
filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
|
||||
filters.ExternalAuthCacheTime = u.Filters.ExternalAuthCacheTime
|
||||
filters.WebClient = make([]string, len(u.Filters.WebClient))
|
||||
copy(filters.WebClient, u.Filters.WebClient)
|
||||
filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))
|
||||
|
|
|
@ -70,6 +70,8 @@ fi
|
|||
|
||||
The structure for SFTPGo users can be found within the [OpenAPI schema](../openapi/openapi.yaml).
|
||||
|
||||
You can instruct SFTPGo to cache the external user by setting an `external_auth_cache_time` in user object returned by your hook. The `external_auth_cache_time` defines the cache time in seconds.
|
||||
|
||||
You can disable the hook on a per-user basis so that you can mix external and internal users.
|
||||
|
||||
An example authentication program allowing to authenticate against an LDAP server can be found inside the source tree [ldapauth](../examples/ldapauth) directory.
|
||||
|
|
6
go.mod
6
go.mod
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/aws/aws-sdk-go v1.43.5
|
||||
github.com/aws/aws-sdk-go v1.43.6
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.8
|
||||
github.com/coreos/go-oidc/v3 v3.1.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
|
@ -41,7 +41,7 @@ require (
|
|||
github.com/rs/cors v1.8.2
|
||||
github.com/rs/xid v1.3.0
|
||||
github.com/rs/zerolog v1.26.2-0.20220203140311-fc26014bd4e1
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220221175917-da8bdf77ce76
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220225104414-9e485ac5bc94
|
||||
github.com/shirou/gopsutil/v3 v3.22.1
|
||||
github.com/spf13/afero v1.8.1
|
||||
github.com/spf13/cobra v1.3.0
|
||||
|
@ -59,7 +59,7 @@ require (
|
|||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
|
||||
google.golang.org/api v0.70.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
|
12
go.sum
12
go.sum
|
@ -144,8 +144,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
|
|||
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.43.5 h1:N7arnx54E4QyW69c45UW5o8j2DCSjzpoxzJW3yU6OSo=
|
||||
github.com/aws/aws-sdk-go v1.43.5/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.43.6 h1:FkwmndZR4LjnT2fiKaD18bnqfQ188E8A1IMNI5rcv00=
|
||||
github.com/aws/aws-sdk-go v1.43.6/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
|
||||
|
@ -698,8 +698,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
|||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220221175917-da8bdf77ce76 h1:6mLGNio6XJaweaKvVmUHLanDznABa2F2PEbS16fWnxg=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220221175917-da8bdf77ce76/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220225104414-9e485ac5bc94 h1:IllQqdyqETJdbik04oorF/oGwSkeY35RTPQjn/eQhO0=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220225104414-9e485ac5bc94/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
|
||||
github.com/shirou/gopsutil/v3 v3.22.1 h1:33y31Q8J32+KstqPfscvFwBlNJ6xLaBy4xqBXzlYV5w=
|
||||
github.com/shirou/gopsutil/v3 v3.22.1/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
|
@ -976,8 +976,8 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
|
|
@ -13942,6 +13942,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
form.Add("hooks", "external_auth_disabled")
|
||||
form.Set("disable_fs_checks", "checked")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
// test invalid url escape
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b)
|
||||
|
@ -14164,6 +14165,15 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
form.Set("download_data_transfer_source12", "100")
|
||||
form.Set("upload_data_transfer_source12", "120")
|
||||
form.Set("total_data_transfer_source12", "200")
|
||||
// invalid external auth cache size
|
||||
form.Set("external_auth_cache_time", "a")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set(csrfFormToken, "invalid form token")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
|
@ -14412,6 +14422,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
form.Set("description", user.Description)
|
||||
form.Set("tls_username", string(sdk.TLSUsernameCN))
|
||||
form.Set("allow_api_key_auth", "1")
|
||||
form.Set("external_auth_cache_time", "120")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
|
@ -14482,6 +14493,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
assert.Equal(t, int64(0), updateUser.TotalDataTransfer)
|
||||
assert.Equal(t, int64(0), updateUser.DownloadDataTransfer)
|
||||
assert.Equal(t, int64(0), updateUser.UploadDataTransfer)
|
||||
assert.Equal(t, int64(0), updateUser.Filters.ExternalAuthCacheTime)
|
||||
if val, ok := updateUser.Permissions["/otherdir"]; ok {
|
||||
assert.True(t, util.IsStringInSlice(dataprovider.PermListItems, val))
|
||||
assert.True(t, util.IsStringInSlice(dataprovider.PermUpload, val))
|
||||
|
@ -14592,6 +14604,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("fs_provider", "0")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("description", "desc %username% %password%")
|
||||
form.Set("vfolder_path", "/vdir%username%")
|
||||
form.Set("vfolder_name", folder.Name)
|
||||
|
@ -14687,6 +14700,7 @@ func TestUserSaveFromTemplateMock(t *testing.T) {
|
|||
form.Set("expiration_date", "")
|
||||
form.Set("fs_provider", "0")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Add("tpl_username", user1)
|
||||
form.Add("tpl_password", "password1")
|
||||
form.Add("tpl_public_keys", " ")
|
||||
|
@ -14761,6 +14775,7 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
|
@ -15109,6 +15124,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
|
@ -15322,6 +15338,7 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
|
@ -15439,6 +15456,7 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
|
@ -15622,6 +15640,7 @@ func TestWebUserCryptMock(t *testing.T) {
|
|||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
|
@ -15727,6 +15746,7 @@ func TestWebUserSFTPFsMock(t *testing.T) {
|
|||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
|
|
|
@ -943,7 +943,8 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
}
|
||||
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
|
||||
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
return filters, nil
|
||||
filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
|
||||
return filters, err
|
||||
}
|
||||
|
||||
func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
|
||||
|
|
|
@ -1518,6 +1518,9 @@ func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User)
|
|||
if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth {
|
||||
return errors.New("allow_api_key_auth mismatch")
|
||||
}
|
||||
if expected.Filters.ExternalAuthCacheTime != actual.Filters.ExternalAuthCacheTime {
|
||||
return errors.New("external_auth_cache_time mismatch")
|
||||
}
|
||||
if err := compareUserFilterSubStructs(expected, actual); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4659,6 +4659,9 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DataTransferLimit'
|
||||
external_auth_cache_time:
|
||||
type: integer
|
||||
description: 'Defines the cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache'
|
||||
description: Additional user options
|
||||
Secret:
|
||||
type: object
|
||||
|
|
|
@ -96,6 +96,7 @@ type Manager struct {
|
|||
hasSearcher bool
|
||||
hasMetadater bool
|
||||
hasNotifiers bool
|
||||
hasAuths bool
|
||||
}
|
||||
|
||||
// Initialize initializes the configured plugins
|
||||
|
@ -174,6 +175,7 @@ func (m *Manager) validateConfigs() error {
|
|||
m.hasSearcher = false
|
||||
m.hasMetadater = false
|
||||
m.hasNotifiers = false
|
||||
m.hasAuths = false
|
||||
|
||||
for _, config := range m.Configs {
|
||||
if config.Type == kmsplugin.PluginName {
|
||||
|
@ -201,10 +203,18 @@ func (m *Manager) validateConfigs() error {
|
|||
if config.Type == notifier.PluginName {
|
||||
m.hasNotifiers = true
|
||||
}
|
||||
if config.Type == auth.PluginName {
|
||||
m.hasAuths = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasAuthenticators returns true if there is at least an auth plugin
|
||||
func (m *Manager) HasAuthenticators() bool {
|
||||
return m.hasAuths
|
||||
}
|
||||
|
||||
// HasNotifiers returns true if there is at least a notifier plugin
|
||||
func (m *Manager) HasNotifiers() bool {
|
||||
return m.hasNotifiers
|
||||
|
|
|
@ -3430,6 +3430,63 @@ func TestLoginExternalAuth(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoginExternalAuthCache(t *testing.T) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
}
|
||||
u := getTestUser(false)
|
||||
u.Filters.ExternalAuthCacheTime = 120
|
||||
err := dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf := config.GetProviderConf()
|
||||
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
providerConf.ExternalAuthHook = extAuthPath
|
||||
providerConf.ExternalAuthScope = 1
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(u, false)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
assert.NoError(t, checkBasicSFTP(client))
|
||||
}
|
||||
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
lastLogin := user.LastLogin
|
||||
assert.Greater(t, lastLogin, int64(0))
|
||||
assert.Equal(t, u.Filters.ExternalAuthCacheTime, user.Filters.ExternalAuthCacheTime)
|
||||
// the auth should be now cached so update the hook to return an error
|
||||
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, true, false, ""), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err = getSftpClient(u, false)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
assert.NoError(t, checkBasicSFTP(client))
|
||||
}
|
||||
user, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, lastLogin, user.LastLogin)
|
||||
|
||||
_, 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, true)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(extAuthPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoginExternalAuthInteractive(t *testing.T) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
|
|
|
@ -876,6 +876,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row {{if not .User.HasExternalAuth}}d-none{{end}}">
|
||||
<label for="idExtAuthCacheTime" class="col-sm-2 col-form-label">External auth cache time</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" min="0" class="form-control" id="idExtAuthCacheTime" name="external_auth_cache_time" placeholder=""
|
||||
value="{{.User.Filters.ExternalAuthCacheTime}}" aria-describedby="extAuthCacheHelpBlock">
|
||||
<small id="extAuthCacheHelpBlock" class="form-text text-muted">
|
||||
Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue