allow to cache external authentications

Fixes #733

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-02-25 11:51:10 +01:00
parent f5a0559be6
commit 4e9dae6fa4
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
13 changed files with 182 additions and 22 deletions

View file

@ -448,8 +448,12 @@ jobs:
name: golangci-lint name: golangci-lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Run golangci-lint - name: Run golangci-lint
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v3
with: with:
version: latest version: latest

View file

@ -1122,7 +1122,11 @@ func UpdateAPIKeyLastUse(apiKey *APIKey) error {
// UpdateLastLogin updates the last login field for the given SFTPGo user // UpdateLastLogin updates the last login field for the given SFTPGo user
func UpdateLastLogin(user *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) err := provider.updateLastLogin(user.Username)
if err == nil { if err == nil {
webDAVUsersCache.updateLastLogin(user.Username) webDAVUsersCache.updateLastLogin(user.Username)
@ -1132,7 +1136,7 @@ func UpdateLastLogin(user *User) {
// UpdateAdminLastLogin updates the last login field for the given SFTPGo admin // UpdateAdminLastLogin updates the last login field for the given SFTPGo admin
func UpdateAdminLastLogin(admin *Admin) { func UpdateAdminLastLogin(admin *Admin) {
if !isLastActivityRecent(admin.LastLogin) { if !isLastActivityRecent(admin.LastLogin, lastLoginMinDelay) {
provider.updateAdminLastLogin(admin.Username) //nolint:errcheck 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)) return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
} }
} }
if !user.HasExternalAuth() {
user.Filters.ExternalAuthCacheTime = 0
}
return validateFiltersPatternExtensions(user) 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 var user User
u, userAsJSON, err := getUserAndJSONForHook(username) u, userAsJSON, err := getUserAndJSONForHook(username)
@ -3219,6 +3228,10 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
return u, nil return u, nil
} }
if u.isExternalAuthCached() {
return u, nil
}
pkey, err := util.GetSSHPublicKeyAsString(pubKey) pkey, err := util.GetSSHPublicKeyAsString(pubKey)
if err != nil { if err != nil {
return user, err return user, err
@ -3295,6 +3308,10 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
return u, nil return u, nil
} }
if u.isExternalAuthCached() {
return u, nil
}
pkey, err := util.GetSSHPublicKeyAsString(pubKey) pkey, err := util.GetSSHPublicKeyAsString(pubKey)
if err != nil { if err != nil {
return user, err return user, err
@ -3368,15 +3385,15 @@ func getUserAndJSONForHook(username string) (User, []byte, error) {
return u, userAsJSON, err return u, userAsJSON, err
} }
func providerLog(level logger.LogLevel, format string, v ...interface{}) { func isLastActivityRecent(lastActivity int64, minDelay time.Duration) bool {
logger.Log(level, logSender, "", format, v...)
}
func isLastActivityRecent(lastActivity int64) bool {
lastActivityTime := util.GetTimeFromMsecSinceEpoch(lastActivity) lastActivityTime := util.GetTimeFromMsecSinceEpoch(lastActivity)
diff := -time.Until(lastActivityTime) diff := -time.Until(lastActivityTime)
if diff < -10*time.Second { if diff < -10*time.Second {
return false return false
} }
return diff < lastLoginMinDelay return diff < minDelay
}
func providerLog(level logger.LogLevel, format string, v ...interface{}) {
logger.Log(level, logSender, "", format, v...)
} }

View file

@ -19,6 +19,7 @@ import (
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/mfa" "github.com/drakkan/sftpgo/v2/mfa"
"github.com/drakkan/sftpgo/v2/plugin"
"github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -203,7 +204,14 @@ func (u *User) CheckFsRoot(connectionID string) error {
if u.Filters.DisableFsChecks { if u.Filters.DisableFsChecks {
return nil 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 return nil
} }
fs, err := u.GetFilesystemForPath("/", connectionID) fs, err := u.GetFilesystemForPath("/", connectionID)
@ -924,6 +932,17 @@ func (u *User) CanManageMFA() bool {
return len(mfa.GetAvailableTOTPConfigs()) > 0 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 // CanManageShares returns true if the user can add, update and list shares
func (u *User) CanManageShares() bool { func (u *User) CanManageShares() bool {
return !util.IsStringInSlice(sdk.WebClientSharesDisabled, u.Filters.WebClient) 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 // HasRecentActivity returns true if the last user login is recent and so we can skip some expensive checks
func (u *User) HasRecentActivity() bool { func (u *User) HasRecentActivity() bool {
return isLastActivityRecent(u.LastLogin) return isLastActivityRecent(u.LastLogin, lastLoginMinDelay)
} }
// HasQuotaRestrictions returns true if there are any disk quota restrictions // 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, ",") 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 // CountUnusedRecoveryCodes returns the number of unused recovery codes
func (u *User) CountUnusedRecoveryCodes() int { func (u *User) CountUnusedRecoveryCodes() int {
unused := 0 unused := 0
@ -1372,6 +1403,7 @@ func (u *User) getACopy() User {
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
filters.DisableFsChecks = u.Filters.DisableFsChecks filters.DisableFsChecks = u.Filters.DisableFsChecks
filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
filters.ExternalAuthCacheTime = u.Filters.ExternalAuthCacheTime
filters.WebClient = make([]string, len(u.Filters.WebClient)) filters.WebClient = make([]string, len(u.Filters.WebClient))
copy(filters.WebClient, u.Filters.WebClient) copy(filters.WebClient, u.Filters.WebClient)
filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes)) filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))

View file

@ -70,6 +70,8 @@ fi
The structure for SFTPGo users can be found within the [OpenAPI schema](../openapi/openapi.yaml). 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. 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. 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
View file

@ -8,7 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 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/cockroachdb/cockroach-go/v2 v2.2.8
github.com/coreos/go-oidc/v3 v3.1.0 github.com/coreos/go-oidc/v3 v3.1.0
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
@ -41,7 +41,7 @@ require (
github.com/rs/cors v1.8.2 github.com/rs/cors v1.8.2
github.com/rs/xid v1.3.0 github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.26.2-0.20220203140311-fc26014bd4e1 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/shirou/gopsutil/v3 v3.22.1
github.com/spf13/afero v1.8.1 github.com/spf13/afero v1.8.1
github.com/spf13/cobra v1.3.0 github.com/spf13/cobra v1.3.0
@ -59,7 +59,7 @@ require (
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 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 google.golang.org/api v0.70.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
) )

12
go.sum
View file

@ -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.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.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.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.6 h1:FkwmndZR4LjnT2fiKaD18bnqfQ188E8A1IMNI5rcv00=
github.com/aws/aws-sdk-go v1.43.5/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 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 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/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= 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/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 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY= 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.20220225104414-9e485ac5bc94 h1:IllQqdyqETJdbik04oorF/oGwSkeY35RTPQjn/eQhO0=
github.com/sftpgo/sdk v0.1.1-0.20220221175917-da8bdf77ce76/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE= 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 h1:33y31Q8J32+KstqPfscvFwBlNJ6xLaBy4xqBXzlYV5w=
github.com/shirou/gopsutil/v3 v3.22.1/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY= 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= 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-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-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-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-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View file

@ -13942,6 +13942,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Add("hooks", "external_auth_disabled") form.Add("hooks", "external_auth_disabled")
form.Set("disable_fs_checks", "checked") form.Set("disable_fs_checks", "checked")
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
// test invalid url escape // test invalid url escape
req, _ = http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b) 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("download_data_transfer_source12", "100")
form.Set("upload_data_transfer_source12", "120") form.Set("upload_data_transfer_source12", "120")
form.Set("total_data_transfer_source12", "200") 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") form.Set(csrfFormToken, "invalid form token")
b, contentType, _ = getMultipartFormData(form, "", "") b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
@ -14412,6 +14422,7 @@ func TestWebUserUpdateMock(t *testing.T) {
form.Set("description", user.Description) form.Set("description", user.Description)
form.Set("tls_username", string(sdk.TLSUsernameCN)) form.Set("tls_username", string(sdk.TLSUsernameCN))
form.Set("allow_api_key_auth", "1") form.Set("allow_api_key_auth", "1")
form.Set("external_auth_cache_time", "120")
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
setJWTCookieForReq(req, webToken) 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.TotalDataTransfer)
assert.Equal(t, int64(0), updateUser.DownloadDataTransfer) assert.Equal(t, int64(0), updateUser.DownloadDataTransfer)
assert.Equal(t, int64(0), updateUser.UploadDataTransfer) assert.Equal(t, int64(0), updateUser.UploadDataTransfer)
assert.Equal(t, int64(0), updateUser.Filters.ExternalAuthCacheTime)
if val, ok := updateUser.Permissions["/otherdir"]; ok { if val, ok := updateUser.Permissions["/otherdir"]; ok {
assert.True(t, util.IsStringInSlice(dataprovider.PermListItems, val)) assert.True(t, util.IsStringInSlice(dataprovider.PermListItems, val))
assert.True(t, util.IsStringInSlice(dataprovider.PermUpload, 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("expiration_date", "2020-01-01 00:00:00")
form.Set("fs_provider", "0") form.Set("fs_provider", "0")
form.Set("max_upload_file_size", "0") form.Set("max_upload_file_size", "0")
form.Set("external_auth_cache_time", "0")
form.Set("description", "desc %username% %password%") form.Set("description", "desc %username% %password%")
form.Set("vfolder_path", "/vdir%username%") form.Set("vfolder_path", "/vdir%username%")
form.Set("vfolder_name", folder.Name) form.Set("vfolder_name", folder.Name)
@ -14687,6 +14700,7 @@ func TestUserSaveFromTemplateMock(t *testing.T) {
form.Set("expiration_date", "") form.Set("expiration_date", "")
form.Set("fs_provider", "0") form.Set("fs_provider", "0")
form.Set("max_upload_file_size", "0") form.Set("max_upload_file_size", "0")
form.Set("external_auth_cache_time", "0")
form.Add("tpl_username", user1) form.Add("tpl_username", user1)
form.Add("tpl_password", "password1") form.Add("tpl_password", "password1")
form.Add("tpl_public_keys", " ") form.Add("tpl_public_keys", " ")
@ -14761,6 +14775,7 @@ func TestUserTemplateMock(t *testing.T) {
form.Set("upload_data_transfer", "0") form.Set("upload_data_transfer", "0")
form.Set("download_data_transfer", "0") form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00") 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("upload_data_transfer", "0")
form.Set("download_data_transfer", "0") form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00") 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("upload_data_transfer", "0")
form.Set("download_data_transfer", "0") form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00") 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("upload_data_transfer", "0")
form.Set("download_data_transfer", "0") form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00") 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("upload_data_transfer", "0")
form.Set("download_data_transfer", "0") form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00") 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("upload_data_transfer", "0")
form.Set("download_data_transfer", "0") form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00") form.Set("expiration_date", "2020-01-01 00:00:00")

View file

@ -943,7 +943,8 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
} }
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0 filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 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 { func getSecretFromFormField(r *http.Request, field string) *kms.Secret {

View file

@ -1518,6 +1518,9 @@ func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User)
if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth { if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth {
return errors.New("allow_api_key_auth mismatch") 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 { if err := compareUserFilterSubStructs(expected, actual); err != nil {
return err return err
} }

View file

@ -4659,6 +4659,9 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/DataTransferLimit' $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 description: Additional user options
Secret: Secret:
type: object type: object

View file

@ -96,6 +96,7 @@ type Manager struct {
hasSearcher bool hasSearcher bool
hasMetadater bool hasMetadater bool
hasNotifiers bool hasNotifiers bool
hasAuths bool
} }
// Initialize initializes the configured plugins // Initialize initializes the configured plugins
@ -174,6 +175,7 @@ func (m *Manager) validateConfigs() error {
m.hasSearcher = false m.hasSearcher = false
m.hasMetadater = false m.hasMetadater = false
m.hasNotifiers = false m.hasNotifiers = false
m.hasAuths = false
for _, config := range m.Configs { for _, config := range m.Configs {
if config.Type == kmsplugin.PluginName { if config.Type == kmsplugin.PluginName {
@ -201,10 +203,18 @@ func (m *Manager) validateConfigs() error {
if config.Type == notifier.PluginName { if config.Type == notifier.PluginName {
m.hasNotifiers = true m.hasNotifiers = true
} }
if config.Type == auth.PluginName {
m.hasAuths = true
}
} }
return nil 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 // HasNotifiers returns true if there is at least a notifier plugin
func (m *Manager) HasNotifiers() bool { func (m *Manager) HasNotifiers() bool {
return m.hasNotifiers return m.hasNotifiers

View file

@ -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) { func TestLoginExternalAuthInteractive(t *testing.T) {
if runtime.GOOS == osWindows { if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows") t.Skip("this test is not available on Windows")

View file

@ -876,6 +876,17 @@
</div> </div>
</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> </div>
</div> </div>