From 4e9dae6fa476d39fa684d3338d8654bee585fda0 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 25 Feb 2022 11:51:10 +0100 Subject: [PATCH] allow to cache external authentications Fixes #733 Signed-off-by: Nicola Murino --- .github/workflows/development.yml | 6 +++- dataprovider/dataprovider.go | 35 ++++++++++++++----- dataprovider/user.go | 36 +++++++++++++++++-- docs/external-auth.md | 2 ++ go.mod | 6 ++-- go.sum | 12 +++---- httpd/httpd_test.go | 20 +++++++++++ httpd/webadmin.go | 3 +- httpdtest/httpdtest.go | 3 ++ openapi/openapi.yaml | 3 ++ plugin/plugin.go | 10 ++++++ sftpd/sftpd_test.go | 57 +++++++++++++++++++++++++++++++ templates/webadmin/user.html | 11 ++++++ 13 files changed, 182 insertions(+), 22 deletions(-) diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index da950322..6ade0bcb 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -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 diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index ec48cf55..dddc3d61 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -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...) } diff --git a/dataprovider/user.go b/dataprovider/user.go index 5264ce67..9a4f5676 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -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)) diff --git a/docs/external-auth.md b/docs/external-auth.md index 7097eb50..cf71160a 100644 --- a/docs/external-auth.md +++ b/docs/external-auth.md @@ -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. diff --git a/go.mod b/go.mod index bc062969..9b8fab12 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 343c02a3..31df53ea 100644 --- a/go.sum +++ b/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= diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index d2898b78..de880e03 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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") diff --git a/httpd/webadmin.go b/httpd/webadmin.go index cd6d1150..42cd8e66 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -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 { diff --git a/httpdtest/httpdtest.go b/httpdtest/httpdtest.go index 949122fe..9f9459e9 100644 --- a/httpdtest/httpdtest.go +++ b/httpdtest/httpdtest.go @@ -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 } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 116a9f89..528616e2 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -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 diff --git a/plugin/plugin.go b/plugin/plugin.go index 19984cfa..1d86a12e 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -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 diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 4046be9b..8bfc4f5b 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -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") diff --git a/templates/webadmin/user.html b/templates/webadmin/user.html index 9ee39289..6e05f8f6 100644 --- a/templates/webadmin/user.html +++ b/templates/webadmin/user.html @@ -876,6 +876,17 @@ +
+ +
+ + + Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache + +
+
+