diff --git a/go.mod b/go.mod index 6691edeb..cae7bab0 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/cockroachdb/cockroach-go/v2 v2.2.16 github.com/coreos/go-oidc/v3 v3.4.0 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 - github.com/fclairamb/ftpserverlib v0.19.1 + github.com/fclairamb/ftpserverlib v0.19.2-0.20220922051837-cde05ddf9fe6 github.com/fclairamb/go-log v0.4.1 github.com/go-acme/lego/v4 v4.8.0 github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6 @@ -70,7 +70,7 @@ require ( golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45 - google.golang.org/api v0.96.0 + google.golang.org/api v0.97.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -156,7 +156,7 @@ require ( golang.org/x/tools v0.1.12 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 // indirect + google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -166,7 +166,6 @@ require ( ) replace ( - github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20220917142547-394d5e183aeb github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b golang.org/x/net => github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895 diff --git a/go.sum b/go.sum index 13db22ed..35b61a36 100644 --- a/go.sum +++ b/go.sum @@ -268,8 +268,6 @@ github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b h1:kCNBtUFKfhiUaE1Z github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro= github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA= github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU= -github.com/drakkan/ftpserverlib v0.0.0-20220917142547-394d5e183aeb h1:sd63fxu7eKejDU0fBiGvVejeEEXNcGv6SVmXrFsDaKM= -github.com/drakkan/ftpserverlib v0.0.0-20220917142547-394d5e183aeb/go.mod h1:Nwsxl2ZzyPiSCgB1rZGnEscTenwkxPhCn1D+Hm/k9JA= github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895 h1:YZkDIISo8YO7PAOX85GYxGCayjBqAutIAjL+XsdEgkc= github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4= @@ -286,6 +284,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fclairamb/ftpserverlib v0.19.2-0.20220922051837-cde05ddf9fe6 h1:WdhM0yDKdtSD+cqWHAMLMTwvUvmzy36eI3Ow8emZmn0= +github.com/fclairamb/ftpserverlib v0.19.2-0.20220922051837-cde05ddf9fe6/go.mod h1:7pR5Ckeygw3T006z1ND6HYSbJz+fTvkFAXlF6snW4yI= github.com/fclairamb/go-log v0.4.1 h1:rLtdSG9x2pK41AIAnE8WYpl05xBJfw1ZyYxZaXFcBsM= github.com/fclairamb/go-log v0.4.1/go.mod h1:sw1KvnkZ4wKCYkvy4SL3qVZcJSWFP8Ure4pM3z+KNn4= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -1123,8 +1123,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1229,8 +1229,8 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 h1:mmbq5q8M1t7dhkLw320YK4PsOXm6jdnUAkErImaIqOg= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ= +google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/httpd/oidc.go b/internal/httpd/oidc.go index 9f8884d1..2fc69ef6 100644 --- a/internal/httpd/oidc.go +++ b/internal/httpd/oidc.go @@ -308,7 +308,7 @@ func (t *oidcToken) isExpired() bool { return t.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) } -func (t *oidcToken) refresh(config OAuth2Config, verifier OIDCTokenVerifier) error { +func (t *oidcToken) refresh(config OAuth2Config, verifier OIDCTokenVerifier, r *http.Request) error { if t.RefreshToken == "" { logger.Debug(logSender, "", "refresh token not set, unable to refresh cookie %#v", t.Cookie) return errors.New("refresh token not set") @@ -363,12 +363,44 @@ func (t *oidcToken) refresh(config OAuth2Config, verifier OIDCTokenVerifier) err if ok { t.SessionID = sid } + err = t.refreshUser(r) + if err != nil { + logger.Debug(logSender, "", "unable to refresh user after token refresh for cookie %#v: %v", t.Cookie, err) + return err + } logger.Debug(logSender, "", "oidc token refreshed for user %#v, cookie %#v", t.Username, t.Cookie) oidcMgr.addToken(*t) return nil } +func (t *oidcToken) refreshUser(r *http.Request) error { + if t.isAdmin() { + admin, err := dataprovider.AdminExists(t.Username) + if err != nil { + return err + } + if err := admin.CanLogin(util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil { + return err + } + t.Permissions = admin.Permissions + t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections + return nil + } + user, err := dataprovider.GetUserWithGroupSettings(t.Username) + if err != nil { + return err + } + if err := user.CheckLoginConditions(); err != nil { + return err + } + if err := checkHTTPClientUser(&user, r, xid.New().String(), true); err != nil { + return err + } + t.Permissions = user.Filters.WebClient + return nil +} + func (t *oidcToken) getUser(r *http.Request) error { if t.isAdmin() { admin, err := dataprovider.AdminExists(t.Username) @@ -438,7 +470,7 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request, } if token.isExpired() { logger.Debug(logSender, "", "oidc token associated with cookie %#v is expired", token.Cookie) - if err = token.refresh(s.binding.OIDC.oauth2Config, s.binding.OIDC.verifier); err != nil { + if err = token.refresh(s.binding.OIDC.oauth2Config, s.binding.OIDC.verifier, r); err != nil { setFlashMessage(w, r, "Your OpenID token is expired, please log-in again") doRedirect() return oidcToken{}, errInvalidToken diff --git a/internal/httpd/oidc_test.go b/internal/httpd/oidc_test.go index 7780c003..a612e23e 100644 --- a/internal/httpd/oidc_test.go +++ b/internal/httpd/oidc_test.go @@ -528,12 +528,16 @@ func TestOIDCLoginLogout(t *testing.T) { func TestOIDCRefreshToken(t *testing.T) { oidcMgr, ok := oidcMgr.(*memoryOIDCManager) require.True(t, ok) + r, err := http.NewRequest(http.MethodGet, webUsersPath, nil) + assert.NoError(t, err) token := oidcToken{ Cookie: xid.New().String(), AccessToken: xid.New().String(), TokenType: "Bearer", ExpiresAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(-1 * time.Minute)), Nonce: xid.New().String(), + Role: adminRoleFieldValue, + Username: defaultAdminUsername, } config := mockOAuth2Config{ tokenSource: &mockTokenSource{ @@ -543,12 +547,12 @@ func TestOIDCRefreshToken(t *testing.T) { verifier := mockOIDCVerifier{ err: common.ErrGenericFailure, } - err := token.refresh(&config, &verifier) + err = token.refresh(&config, &verifier, r) if assert.Error(t, err) { assert.Contains(t, err.Error(), "refresh token not set") } token.RefreshToken = xid.New().String() - err = token.refresh(&config, &verifier) + err = token.refresh(&config, &verifier, r) assert.ErrorIs(t, err, common.ErrGenericFailure) newToken := &oauth2.Token{ @@ -564,7 +568,7 @@ func TestOIDCRefreshToken(t *testing.T) { verifier = mockOIDCVerifier{ token: &oidc.IDToken{}, } - err = token.refresh(&config, &verifier) + err = token.refresh(&config, &verifier, r) if assert.Error(t, err) { assert.Contains(t, err.Error(), "the refreshed token has no id token") } @@ -580,7 +584,7 @@ func TestOIDCRefreshToken(t *testing.T) { verifier = mockOIDCVerifier{ err: common.ErrGenericFailure, } - err = token.refresh(&config, &verifier) + err = token.refresh(&config, &verifier, r) assert.ErrorIs(t, err, common.ErrGenericFailure) newToken = newToken.WithExtra(map[string]any{ @@ -595,7 +599,7 @@ func TestOIDCRefreshToken(t *testing.T) { verifier = mockOIDCVerifier{ token: &oidc.IDToken{}, } - err = token.refresh(&config, &verifier) + err = token.refresh(&config, &verifier, r) if assert.Error(t, err) { assert.Contains(t, err.Error(), "the refreshed token nonce mismatch") } @@ -604,7 +608,7 @@ func TestOIDCRefreshToken(t *testing.T) { Nonce: token.Nonce, }, } - err = token.refresh(&config, &verifier) + err = token.refresh(&config, &verifier, r) if assert.Error(t, err) { assert.Contains(t, err.Error(), "oidc: claims not set") } @@ -615,13 +619,114 @@ func TestOIDCRefreshToken(t *testing.T) { verifier = mockOIDCVerifier{ token: idToken, } - err = token.refresh(&config, &verifier) + err = token.refresh(&config, &verifier, r) assert.NoError(t, err) + assert.Len(t, token.Permissions, 1) + token.Role = nil + // user does not exist + err = token.refresh(&config, &verifier, r) + assert.Error(t, err) require.Len(t, oidcMgr.tokens, 1) oidcMgr.removeToken(token.Cookie) require.Len(t, oidcMgr.tokens, 0) } +func TestOIDCRefreshUser(t *testing.T) { + token := oidcToken{ + Cookie: xid.New().String(), + AccessToken: xid.New().String(), + TokenType: "Bearer", + ExpiresAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Minute)), + Nonce: xid.New().String(), + Role: adminRoleFieldValue, + Username: "missing username", + } + r, err := http.NewRequest(http.MethodGet, webUsersPath, nil) + assert.NoError(t, err) + err = token.refreshUser(r) + assert.Error(t, err) + admin := dataprovider.Admin{ + Username: "test_oidc_admin_refresh", + Password: "p", + Permissions: []string{dataprovider.PermAdminAny}, + Status: 0, + Filters: dataprovider.AdminFilters{ + Preferences: dataprovider.AdminPreferences{ + HideUserPageSections: 1 + 2 + 4, + }, + }, + } + err = dataprovider.AddAdmin(&admin, "", "") + assert.NoError(t, err) + + token.Username = admin.Username + err = token.refreshUser(r) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "is disabled") + } + + admin.Status = 1 + err = dataprovider.UpdateAdmin(&admin, "", "") + assert.NoError(t, err) + err = token.refreshUser(r) + assert.NoError(t, err) + assert.Equal(t, admin.Permissions, token.Permissions) + assert.Equal(t, admin.Filters.Preferences.HideUserPageSections, token.HideUserPageSections) + + err = dataprovider.DeleteAdmin(admin.Username, "", "") + assert.NoError(t, err) + + username := "test_oidc_user_refresh_token" + user := dataprovider.User{ + BaseUser: sdk.BaseUser{ + Username: username, + Password: "p", + HomeDir: filepath.Join(os.TempDir(), username), + Status: 0, + Permissions: map[string][]string{ + "/": {dataprovider.PermAny}, + }, + }, + Filters: dataprovider.UserFilters{ + BaseUserFilters: sdk.BaseUserFilters{ + DeniedProtocols: []string{common.ProtocolHTTP}, + WebClient: []string{sdk.WebClientSharesDisabled, sdk.WebClientWriteDisabled}, + }, + }, + } + err = dataprovider.AddUser(&user, "", "") + assert.NoError(t, err) + + r, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil) + assert.NoError(t, err) + token.Role = nil + token.Username = username + assert.False(t, token.isAdmin()) + err = token.refreshUser(r) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "is disabled") + } + user, err = dataprovider.UserExists(username) + assert.NoError(t, err) + user.Status = 1 + err = dataprovider.UpdateUser(&user, "", "") + assert.NoError(t, err) + err = token.refreshUser(r) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "protocol HTTP is not allowed") + } + + user.Filters.DeniedProtocols = []string{common.ProtocolFTP} + err = dataprovider.UpdateUser(&user, "", "") + assert.NoError(t, err) + err = token.refreshUser(r) + assert.NoError(t, err) + assert.Equal(t, user.Filters.WebClient, token.Permissions) + + err = dataprovider.DeleteUser(username, "", "") + assert.NoError(t, err) +} + func TestValidateOIDCToken(t *testing.T) { oidcMgr, ok := oidcMgr.(*memoryOIDCManager) require.True(t, ok)