From 5efc3386d3fcd933a4a450a038b72a23543a9e83 Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 12 Jul 2023 12:31:36 +0200 Subject: [PATCH] AuthZ: Extend /api/search to work with self-contained permissions (#70749) * Search sql filter draft, unfinished * Search works for empty roles * Add current AuthModule to SignedInUser * clean up, changes to the search * Use constant prefixes * Change AuthModule to AuthenticatedBy * Add tests for using the permissions from the SignedInUser * Refactor and simplify code * Fix sql generation for pg and mysql * Fixes, clean up * Add test for empty permission list * Fix * Fix any vs all in case of edit permission * Update pkg/services/authn/authn.go Co-authored-by: Gabriel MABILLE * Update pkg/services/sqlstore/permissions/dashboard_test.go Co-authored-by: Gabriel MABILLE * Fixes, changes based on the review --------- Co-authored-by: Gabriel MABILLE --- pkg/services/authn/authn.go | 72 ++-- .../authn/authnimpl/sync/user_sync.go | 26 +- .../authn/authnimpl/sync/user_sync_test.go | 66 ++-- pkg/services/authn/clients/api_key.go | 12 +- pkg/services/authn/clients/api_key_test.go | 3 + pkg/services/authn/clients/ext_jwt.go | 3 +- pkg/services/authn/clients/ext_jwt_test.go | 26 +- pkg/services/authn/clients/grafana.go | 6 +- pkg/services/authn/clients/grafana_test.go | 36 +- pkg/services/authn/clients/jwt.go | 6 +- pkg/services/authn/clients/jwt_test.go | 26 +- pkg/services/authn/clients/ldap.go | 18 +- pkg/services/authn/clients/ldap_test.go | 32 +- pkg/services/authn/clients/oauth.go | 18 +- pkg/services/authn/clients/oauth_test.go | 30 +- pkg/services/authn/clients/proxy.go | 3 +- pkg/services/authn/clients/render.go | 4 +- pkg/services/authn/clients/render_test.go | 24 +- pkg/services/contexthandler/contexthandler.go | 2 +- pkg/services/login/authinfo.go | 3 + .../sqlstore/permissions/dashboard.go | 156 +++++++-- .../sqlstore/permissions/dashboard_test.go | 312 ++++++++++++++++++ pkg/services/user/model.go | 1 + 23 files changed, 650 insertions(+), 235 deletions(-) diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index f2b84ea7559..39053aa9da9 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -200,9 +200,9 @@ type Identity struct { Email string // IsGrafanaAdmin is true if the entity is a Grafana admin. IsGrafanaAdmin *bool - // AuthModule is the name of the external system. For example, "auth_ldap" or "auth_saml". - // Empty if the identity is provided by Grafana. - AuthModule string + // AuthenticatedBy is the name of the authentication client that was used to authenticate the current Identity. + // For example, "password", "apikey", "auth_ldap" or "auth_azuread". + AuthenticatedBy string // AuthId is the unique identifier for the entity in the external system. // Empty if the identity is provided by Grafana. AuthID string @@ -262,21 +262,22 @@ func (i *Identity) SignedInUser() *user.SignedInUser { } u := &user.SignedInUser{ - UserID: 0, - OrgID: i.OrgID, - OrgName: i.OrgName, - OrgRole: i.Role(), - Login: i.Login, - Name: i.Name, - Email: i.Email, - OrgCount: i.OrgCount, - IsGrafanaAdmin: isGrafanaAdmin, - IsAnonymous: i.IsAnonymous, - IsDisabled: i.IsDisabled, - HelpFlags1: i.HelpFlags1, - LastSeenAt: i.LastSeenAt, - Teams: i.Teams, - Permissions: i.Permissions, + UserID: 0, + OrgID: i.OrgID, + OrgName: i.OrgName, + OrgRole: i.Role(), + Login: i.Login, + Name: i.Name, + Email: i.Email, + AuthenticatedBy: i.AuthenticatedBy, + OrgCount: i.OrgCount, + IsGrafanaAdmin: isGrafanaAdmin, + IsAnonymous: i.IsAnonymous, + IsDisabled: i.IsDisabled, + HelpFlags1: i.HelpFlags1, + LastSeenAt: i.LastSeenAt, + Teams: i.Teams, + Permissions: i.Permissions, } namespace, id := i.NamespacedID() @@ -294,7 +295,7 @@ func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { _, id := i.NamespacedID() return login.ExternalUserInfo{ OAuthToken: i.OAuthToken, - AuthModule: i.AuthModule, + AuthModule: i.AuthenticatedBy, AuthId: i.AuthID, UserId: id, Email: i.Email, @@ -308,23 +309,24 @@ func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { } // IdentityFromSignedInUser creates an identity from a SignedInUser. -func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams) *Identity { +func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams, authenticatedBy string) *Identity { return &Identity{ - ID: id, - OrgID: usr.OrgID, - OrgName: usr.OrgName, - OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole}, - Login: usr.Login, - Name: usr.Name, - Email: usr.Email, - OrgCount: usr.OrgCount, - IsGrafanaAdmin: &usr.IsGrafanaAdmin, - IsDisabled: usr.IsDisabled, - HelpFlags1: usr.HelpFlags1, - LastSeenAt: usr.LastSeenAt, - Teams: usr.Teams, - ClientParams: params, - Permissions: usr.Permissions, + ID: id, + OrgID: usr.OrgID, + OrgName: usr.OrgName, + OrgRoles: map[int64]org.RoleType{usr.OrgID: usr.OrgRole}, + Login: usr.Login, + Name: usr.Name, + Email: usr.Email, + AuthenticatedBy: authenticatedBy, + OrgCount: usr.OrgCount, + IsGrafanaAdmin: &usr.IsGrafanaAdmin, + IsDisabled: usr.IsDisabled, + HelpFlags1: usr.HelpFlags1, + LastSeenAt: usr.LastSeenAt, + Teams: usr.Teams, + ClientParams: params, + Permissions: usr.Permissions, } } diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index b4b38e2dbcc..b2ea898cd99 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -76,13 +76,13 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth // Does user exist in the database? usr, userAuth, errUserInDB := s.getUser(ctx, id) if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) { - s.log.FromContext(ctx).Error("Failed to fetch user", "error", errUserInDB, "auth_module", id.AuthModule, "auth_id", id.AuthID) + s.log.FromContext(ctx).Error("Failed to fetch user", "error", errUserInDB, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) return errSyncUserInternal.Errorf("unable to retrieve user") } if errors.Is(errUserInDB, user.ErrUserNotFound) { if !id.ClientParams.AllowSignUp { - s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthModule, "auth_id", id.AuthID) + s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) return errUserSignupDisabled.Errorf("%w", login.ErrSignupNotAllowed) } @@ -90,13 +90,13 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth var errCreate error usr, errCreate = s.createUser(ctx, id) if errCreate != nil { - s.log.FromContext(ctx).Error("Failed to create user", "error", errCreate, "auth_module", id.AuthModule, "auth_id", id.AuthID) + s.log.FromContext(ctx).Error("Failed to create user", "error", errCreate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) return errSyncUserInternal.Errorf("unable to create user") } } else { // update user if errUpdate := s.updateUserAttributes(ctx, usr, id, userAuth); errUpdate != nil { - s.log.FromContext(ctx).Error("Failed to update user", "error", errUpdate, "auth_module", id.AuthModule, "auth_id", id.AuthID) + s.log.FromContext(ctx).Error("Failed to update user", "error", errUpdate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) return errSyncUserInternal.Errorf("unable to update user") } } @@ -174,7 +174,7 @@ func (s *UserSync) EnableDisabledUserHook(ctx context.Context, identity *authn.I } func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, identity *authn.Identity, createConnection bool) error { - if identity.AuthModule == "" { + if identity.AuthenticatedBy == "" { return nil } @@ -184,7 +184,7 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident if createConnection { return s.authInfoService.SetAuthInfo(ctx, &login.SetAuthInfoCommand{ UserId: userID, - AuthModule: identity.AuthModule, + AuthModule: identity.AuthenticatedBy, AuthId: identity.AuthID, OAuthToken: identity.OAuthToken, }) @@ -194,13 +194,13 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident return s.authInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{ UserId: userID, AuthId: identity.AuthID, - AuthModule: identity.AuthModule, + AuthModule: identity.AuthenticatedBy, OAuthToken: identity.OAuthToken, }) } func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id *authn.Identity, userAuth *login.UserAuth) error { - if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthModule); errProtection != nil { + if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthenticatedBy); errProtection != nil { return errUserProtection.Errorf("user mapping not allowed: %w", errProtection) } // sync user info @@ -286,8 +286,8 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user.User, *login.UserAuth, error) { // Check auth info fist - if identity.AuthID != "" && identity.AuthModule != "" { - query := &login.GetAuthInfoQuery{AuthId: identity.AuthID, AuthModule: identity.AuthModule} + if identity.AuthID != "" && identity.AuthenticatedBy != "" { + query := &login.GetAuthInfoQuery{AuthId: identity.AuthID, AuthModule: identity.AuthenticatedBy} authInfo, errGetAuthInfo := s.authInfoService.GetAuthInfo(ctx, query) if errGetAuthInfo != nil && !errors.Is(errGetAuthInfo, user.ErrUserNotFound) { @@ -307,7 +307,7 @@ func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user // if the user connected to user auth does not exist try to clean it up if errors.Is(errGetByID, user.ErrUserNotFound) { if err := s.authInfoService.DeleteUserAuthInfo(ctx, authInfo.UserId); err != nil { - s.log.FromContext(ctx).Error("Failed to clean up user auth", "error", err, "auth_module", identity.AuthModule, "auth_id", identity.AuthID) + s.log.FromContext(ctx).Error("Failed to clean up user auth", "error", err, "auth_module", identity.AuthenticatedBy, "auth_id", identity.AuthID) } } } @@ -322,8 +322,8 @@ func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user var userAuth *login.UserAuth // Special case for generic oauth: generic oauth does not store authID, // so we need to find the user first then check for the userAuth connection by module and userID - if identity.AuthModule == login.GenericOAuthModule { - query := &login.GetAuthInfoQuery{AuthModule: identity.AuthModule, UserId: usr.ID} + if identity.AuthenticatedBy == login.GenericOAuthModule { + query := &login.GetAuthInfoQuery{AuthModule: identity.AuthenticatedBy, UserId: usr.ID} userAuth, err = s.authInfoService.GetAuthInfo(ctx, query) if err != nil && !errors.Is(err, user.ErrUserNotFound) { return nil, nil, err diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index 60ed215f68a..bbaf662e74b 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -268,12 +268,12 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", - AuthID: "2032", - AuthModule: "oauth", - Login: "test", - Name: "test", - Email: "test", + ID: "", + AuthID: "2032", + AuthenticatedBy: "oauth", + Login: "test", + Name: "test", + Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, LookUpParams: login.UserLookupParams{ @@ -286,13 +286,13 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:1", - AuthID: "2032", - AuthModule: "oauth", - Login: "test", - Name: "test", - Email: "test", - IsGrafanaAdmin: ptrBool(false), + ID: "user:1", + AuthID: "2032", + AuthenticatedBy: "oauth", + Login: "test", + Name: "test", + Email: "test", + IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, LookUpParams: login.UserLookupParams{ @@ -313,12 +313,12 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", - Login: "test", - Name: "test", - Email: "test", - AuthModule: "oauth", - AuthID: "2032", + ID: "", + Login: "test", + Name: "test", + Email: "test", + AuthenticatedBy: "oauth", + AuthID: "2032", ClientParams: authn.ClientParams{ SyncUser: true, LookUpParams: login.UserLookupParams{ @@ -341,13 +341,13 @@ func TestUserSync_SyncUserHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: "", - Login: "test_create", - Name: "test_create", - IsGrafanaAdmin: ptrBool(true), - Email: "test_create", - AuthModule: "oauth", - AuthID: "2032", + ID: "", + Login: "test_create", + Name: "test_create", + IsGrafanaAdmin: ptrBool(true), + Email: "test_create", + AuthenticatedBy: "oauth", + AuthID: "2032", ClientParams: authn.ClientParams{ SyncUser: true, AllowSignUp: true, @@ -362,13 +362,13 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: "user:2", - Login: "test_create", - Name: "test_create", - Email: "test_create", - AuthModule: "oauth", - AuthID: "2032", - IsGrafanaAdmin: ptrBool(true), + ID: "user:2", + Login: "test_create", + Name: "test_create", + Email: "test_create", + AuthenticatedBy: "oauth", + AuthID: "2032", + IsGrafanaAdmin: ptrBool(true), ClientParams: authn.ClientParams{ SyncUser: true, AllowSignUp: true, diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index 813e902b8bc..23a8bac4a44 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" @@ -64,10 +65,11 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide // if the api key don't belong to a service account construct the identity and return it if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 { return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID), - OrgID: apiKey.OrgID, - OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role}, - ClientParams: authn.ClientParams{SyncPermissions: true}, + ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID), + OrgID: apiKey.OrgID, + OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role}, + ClientParams: authn.ClientParams{SyncPermissions: true}, + AuthenticatedBy: login.APIKeyAuthModule, }, nil } @@ -80,7 +82,7 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide return nil, err } - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.APIKeyAuthModule), nil } func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) { diff --git a/pkg/services/authn/clients/api_key_test.go b/pkg/services/authn/clients/api_key_test.go index cdfa76c9afb..56a7c081a1b 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -55,6 +56,7 @@ func TestAPIKey_Authenticate(t *testing.T) { ClientParams: authn.ClientParams{ SyncPermissions: true, }, + AuthenticatedBy: login.APIKeyAuthModule, }, }, { @@ -88,6 +90,7 @@ func TestAPIKey_Authenticate(t *testing.T) { ClientParams: authn.ClientParams{ SyncPermissions: true, }, + AuthenticatedBy: login.APIKeyAuthModule, }, }, { diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 8fb21d16910..1932598122f 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthserver" "github.com/grafana/grafana/pkg/services/signingkeys" "github.com/grafana/grafana/pkg/services/user" @@ -100,7 +101,7 @@ func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*auth signedInUser.Permissions[s.getDefaultOrgID()] = claims.Entitlements - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: false}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: false}, login.ExtendedJWTModule), nil } func (s *ExtendedJWT) Test(ctx context.Context, r *authn.Request) bool { diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index d88ead74738..205fb52c0dc 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -150,19 +150,19 @@ func TestExtendedJWT_Authenticate(t *testing.T) { } }, want: &authn.Identity{ - OrgID: 1, - OrgCount: 0, - OrgName: "", - OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, - ID: "user:2", - Login: "johndoe", - Name: "John Doe", - Email: "johndoe@grafana.com", - IsGrafanaAdmin: boolPtr(false), - AuthModule: "", - AuthID: "", - IsDisabled: false, - HelpFlags1: 0, + OrgID: 1, + OrgCount: 0, + OrgName: "", + OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, + ID: "user:2", + Login: "johndoe", + Name: "John Doe", + Email: "johndoe@grafana.com", + IsGrafanaAdmin: boolPtr(false), + AuthenticatedBy: login.ExtendedJWTModule, + AuthID: "", + IsDisabled: false, + HelpFlags1: 0, Permissions: map[int64]map[string][]string{ 1: { "dashboards:create": { diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index 88cd8f0761b..6f16e3e910d 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -32,8 +32,8 @@ func (c *Grafana) String() string { func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { identity := &authn.Identity{ - AuthModule: login.AuthProxyAuthModule, - AuthID: username, + AuthenticatedBy: login.AuthProxyAuthModule, + AuthID: username, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, @@ -109,7 +109,7 @@ func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, us return nil, err } - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}, login.PasswordAuthModule), nil } func comparePassword(password, salt, hash string) bool { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index f2cd1503345..5fc79b3c478 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -40,13 +40,13 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { proxyFieldEmail: "email@email.com", }, expectedIdentity: &authn.Identity{ - OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, - Login: "test", - Name: "name", - Email: "email@email.com", - AuthModule: "authproxy", - AuthID: "test", - Groups: []string{"grp1", "grp2"}, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + Login: "test", + Name: "name", + Email: "email@email.com", + AuthenticatedBy: login.AuthProxyAuthModule, + AuthID: "test", + Groups: []string{"grp1", "grp2"}, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, @@ -66,10 +66,10 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}}, additional: map[string]string{}, expectedIdentity: &authn.Identity{ - Login: "test@test.com", - Email: "test@test.com", - AuthModule: "authproxy", - AuthID: "test@test.com", + Login: "test@test.com", + Email: "test@test.com", + AuthenticatedBy: login.AuthProxyAuthModule, + AuthID: "test@test.com", ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, @@ -106,7 +106,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { assert.Equal(t, tt.expectedIdentity.Name, identity.Name) assert.Equal(t, tt.expectedIdentity.Email, identity.Email) assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID) - assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule) + assert.Equal(t, tt.expectedIdentity.AuthenticatedBy, identity.AuthenticatedBy) assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups) assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser) @@ -143,11 +143,13 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { findUser: true, expectedSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: "Viewer"}, expectedIdentity: &authn.Identity{ - ID: "user:1", - OrgID: 1, - OrgRoles: map[int64]org.RoleType{1: "Viewer"}, - IsGrafanaAdmin: boolPtr(false), - ClientParams: authn.ClientParams{SyncPermissions: true}}, + ID: "user:1", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: "Viewer"}, + IsGrafanaAdmin: boolPtr(false), + ClientParams: authn.ClientParams{SyncPermissions: true}, + AuthenticatedBy: login.PasswordAuthModule, + }, }, { desc: "should fail for incorrect password", diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index cc65b7e4ed2..0f5fad2d64c 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -66,9 +66,9 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi } id := &authn.Identity{ - AuthModule: login.JWTModule, - AuthID: sub, - OrgRoles: map[int64]org.RoleType{}, + AuthenticatedBy: login.JWTModule, + AuthID: sub, + OrgRoles: map[int64]org.RoleType{}, ClientParams: authn.ClientParams{ SyncUser: true, FetchSyncedUser: true, diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index f59aac88d8e..958113596ab 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -35,19 +35,19 @@ func TestAuthenticateJWT(t *testing.T) { } jwtHeaderName := "X-Forwarded-User" wantID := &authn.Identity{ - OrgID: 0, - OrgCount: 0, - OrgName: "", - OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, - ID: "", - Login: "eai-doe", - Name: "Eai Doe", - Email: "eai.doe@cor.po", - IsGrafanaAdmin: boolPtr(false), - AuthModule: "jwt", - AuthID: "1234567890", - IsDisabled: false, - HelpFlags1: 0, + OrgID: 0, + OrgCount: 0, + OrgName: "", + OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, + ID: "", + Login: "eai-doe", + Name: "Eai Doe", + Email: "eai.doe@cor.po", + IsGrafanaAdmin: boolPtr(false), + AuthenticatedBy: login.JWTModule, + AuthID: "1234567890", + IsDisabled: false, + HelpFlags1: 0, ClientParams: authn.ClientParams{ SyncUser: true, AllowSignUp: true, diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 76013eb9c34..e4da257d0b8 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -71,15 +71,15 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern func (c *LDAP) identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo) *authn.Identity { return &authn.Identity{ - OrgID: orgID, - OrgRoles: info.OrgRoles, - Login: info.Login, - Name: info.Name, - Email: info.Email, - IsGrafanaAdmin: info.IsGrafanaAdmin, - AuthModule: info.AuthModule, - AuthID: info.AuthId, - Groups: info.Groups, + OrgID: orgID, + OrgRoles: info.OrgRoles, + Login: info.Login, + Name: info.Name, + Email: info.Email, + IsGrafanaAdmin: info.IsGrafanaAdmin, + AuthenticatedBy: info.AuthModule, + AuthID: info.AuthId, + Groups: info.Groups, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index 0650550ecb8..2bc5086e232 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -39,14 +39,14 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, }, expectedIdentity: &authn.Identity{ - OrgID: 1, - OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, - Login: "test", - Name: "test test", - Email: "test@test.com", - AuthModule: login.LDAPAuthModule, - AuthID: "123", - Groups: []string{"1", "2"}, + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + Login: "test", + Name: "test test", + Email: "test@test.com", + AuthenticatedBy: login.LDAPAuthModule, + AuthID: "123", + Groups: []string{"1", "2"}, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, @@ -105,14 +105,14 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, }, expectedIdentity: &authn.Identity{ - OrgID: 1, - OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, - Login: "test", - Name: "test test", - Email: "test@test.com", - AuthModule: login.LDAPAuthModule, - AuthID: "123", - Groups: []string{"1", "2"}, + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + Login: "test", + Name: "test test", + Email: "test@test.com", + AuthenticatedBy: login.LDAPAuthModule, + AuthID: "123", + Groups: []string{"1", "2"}, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 5109b89aa68..d3483af89af 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -146,15 +146,15 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden } return &authn.Identity{ - Login: userInfo.Login, - Name: userInfo.Name, - Email: userInfo.Email, - IsGrafanaAdmin: isGrafanaAdmin, - AuthModule: c.moduleName, - AuthID: userInfo.Id, - Groups: userInfo.Groups, - OAuthToken: token, - OrgRoles: orgRoles, + Login: userInfo.Login, + Name: userInfo.Name, + Email: userInfo.Email, + IsGrafanaAdmin: isGrafanaAdmin, + AuthenticatedBy: c.moduleName, + AuthID: userInfo.Id, + Groups: userInfo.Groups, + OAuthToken: token, + OrgRoles: orgRoles, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, diff --git a/pkg/services/authn/clients/oauth_test.go b/pkg/services/authn/clients/oauth_test.go index b42017d4fc1..d812bdcffdb 100644 --- a/pkg/services/authn/clients/oauth_test.go +++ b/pkg/services/authn/clients/oauth_test.go @@ -129,13 +129,13 @@ func TestOAuth_Authenticate(t *testing.T) { Groups: []string{"grp1", "grp2"}, }, expectedIdentity: &authn.Identity{ - Email: "some@email.com", - AuthModule: "oauth_azuread", - AuthID: "123", - Name: "name", - Groups: []string{"grp1", "grp2"}, - OAuthToken: &oauth2.Token{}, - OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + Email: "some@email.com", + AuthenticatedBy: login.AzureADAuthModule, + AuthID: "123", + Name: "name", + Groups: []string{"grp1", "grp2"}, + OAuthToken: &oauth2.Token{}, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, @@ -168,13 +168,13 @@ func TestOAuth_Authenticate(t *testing.T) { Groups: []string{"grp1", "grp2"}, }, expectedIdentity: &authn.Identity{ - Email: "some@email.com", - AuthModule: "oauth_azuread", - AuthID: "123", - Name: "name", - Groups: []string{"grp1", "grp2"}, - OAuthToken: &oauth2.Token{}, - OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + Email: "some@email.com", + AuthenticatedBy: login.AzureADAuthModule, + AuthID: "123", + Name: "name", + Groups: []string{"grp1", "grp2"}, + OAuthToken: &oauth2.Token{}, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, ClientParams: authn.ClientParams{ SyncUser: true, SyncTeams: true, @@ -221,7 +221,7 @@ func TestOAuth_Authenticate(t *testing.T) { assert.Equal(t, tt.expectedIdentity.Name, identity.Name) assert.Equal(t, tt.expectedIdentity.Email, identity.Email) assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID) - assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule) + assert.Equal(t, tt.expectedIdentity.AuthenticatedBy, identity.AuthenticatedBy) assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups) assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser) diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index c7384eb898b..c79f45f8583 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -99,7 +100,7 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden // and perform syncs if usr != nil { c.log.FromContext(ctx).Debug("User was loaded from cache, skip syncs", "userId", usr.UserID) - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.AuthProxyAuthModule), nil } } } diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index 23affc2c978..5e37372f826 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -56,11 +56,11 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide return nil, err } - identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}) + identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.RenderModule) } identity.LastSeenAt = time.Now() - identity.AuthModule = login.RenderModule + identity.AuthenticatedBy = login.RenderModule return identity, nil } diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go index 4a1a9728948..55be7e38008 100644 --- a/pkg/services/authn/clients/render_test.go +++ b/pkg/services/authn/clients/render_test.go @@ -38,11 +38,11 @@ func TestRender_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: "user:0", - OrgID: 1, - OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, - AuthModule: login.RenderModule, - ClientParams: authn.ClientParams{SyncPermissions: true}, + ID: "user:0", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + AuthenticatedBy: login.RenderModule, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, @@ -59,13 +59,13 @@ func TestRender_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: "user:1", - OrgID: 1, - OrgName: "test", - OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, - IsGrafanaAdmin: boolPtr(false), - AuthModule: login.RenderModule, - ClientParams: authn.ClientParams{SyncPermissions: true}, + ID: "user:1", + OrgID: 1, + OrgName: "test", + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + IsGrafanaAdmin: boolPtr(false), + AuthenticatedBy: login.RenderModule, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 5b9ed22e3cb..2833a0e7431 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -185,7 +185,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { reqContext.UserToken = identity.SessionToken reqContext.IsSignedIn = !identity.IsAnonymous reqContext.AllowAnonymous = identity.IsAnonymous - reqContext.IsRenderCall = identity.AuthModule == login.RenderModule + reqContext.IsRenderCall = identity.AuthenticatedBy == login.RenderModule } } else { const headerName = "X-Grafana-Org-Id" diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index 5bef4ca3059..c279cafe384 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -36,10 +36,13 @@ type Store interface { const ( // modules + PasswordAuthModule = "password" + APIKeyAuthModule = "apikey" SAMLAuthModule = "auth.saml" LDAPAuthModule = "ldap" AuthProxyAuthModule = "authproxy" JWTModule = "jwt" + ExtendedJWTModule = "extendedjwt" RenderModule = "render" // OAuth provider modules AzureADAuthModule = "oauth_azuread" diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 0a1501795c1..d12085c6f0c 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" @@ -172,36 +173,68 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { permSelector := strings.Builder{} var permSelectorArgs []interface{} + // useSelfContainedPermissions is true if the user's permissions are stored and set from the JWT token + // currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana) + useSelfContainedPermissions := f.user.AuthenticatedBy == login.ExtendedJWTModule + if len(f.dashboardActions) > 0 { toCheck := actionsToCheck(f.dashboardActions, f.user.Permissions[f.user.OrgID], dashWildcards, folderWildcards) if len(toCheck) > 0 { - builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'") - builder.WriteString(rolesFilter) - args = append(args, params...) + if !useSelfContainedPermissions { + builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'") + builder.WriteString(rolesFilter) + args = append(args, params...) - if len(toCheck) == 1 { - builder.WriteString(" AND action = ?") - args = append(args, toCheck[0]) + if len(toCheck) == 1 { + builder.WriteString(" AND action = ?") + args = append(args, toCheck[0]) + } else { + builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + args = append(args, toCheck...) + args = append(args, len(toCheck)) + } + builder.WriteString(") AND NOT dashboard.is_folder)") } else { - builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") - args = append(args, toCheck...) - args = append(args, len(toCheck)) + actions := parseStringSliceFromInterfaceSlice(toCheck) + + args = getAllowedUIDs(actions, f.user, dashboards.ScopeDashboardsPrefix) + + // Only add the IN clause if we have any dashboards to check + if len(args) > 0 { + builder.WriteString("(dashboard.uid IN (?" + strings.Repeat(", ?", len(args)-1) + "") + builder.WriteString(") AND NOT dashboard.is_folder)") + } else { + builder.WriteString("(1 = 0)") + } } - builder.WriteString(") AND NOT dashboard.is_folder)") builder.WriteString(" OR ") - permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ") - permSelector.WriteString(rolesFilter) - permSelectorArgs = append(permSelectorArgs, params...) - if len(toCheck) == 1 { - permSelector.WriteString(" AND action = ?") - permSelectorArgs = append(permSelectorArgs, toCheck[0]) + if !useSelfContainedPermissions { + permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ") + permSelector.WriteString(rolesFilter) + permSelectorArgs = append(permSelectorArgs, params...) + + if len(toCheck) == 1 { + permSelector.WriteString(" AND action = ?") + permSelectorArgs = append(permSelectorArgs, toCheck[0]) + } else { + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + permSelectorArgs = append(permSelectorArgs, toCheck...) + permSelectorArgs = append(permSelectorArgs, len(toCheck)) + } } else { - permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") - permSelectorArgs = append(permSelectorArgs, toCheck...) - permSelectorArgs = append(permSelectorArgs, len(toCheck)) + actions := parseStringSliceFromInterfaceSlice(toCheck) + + permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix) + + // Only add the IN clause if we have any folders to check + if len(permSelectorArgs) > 0 { + permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "") + } else { + permSelector.WriteString("(") + } } permSelector.WriteRune(')') @@ -221,9 +254,13 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { } default: builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ") - builder.WriteString("WHERE d.uid IN ") - builder.WriteString(permSelector.String()) - args = append(args, permSelectorArgs...) + if len(permSelectorArgs) > 0 { + builder.WriteString("WHERE d.uid IN ") + builder.WriteString(permSelector.String()) + args = append(args, permSelectorArgs...) + } else { + builder.WriteString("WHERE 1 = 0") + } } builder.WriteString(") AND NOT dashboard.is_folder)") } else { @@ -242,17 +279,30 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { toCheck := actionsToCheck(f.folderActions, f.user.Permissions[f.user.OrgID], folderWildcards) if len(toCheck) > 0 { - permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'") - permSelector.WriteString(rolesFilter) - permSelectorArgs = append(permSelectorArgs, params...) - if len(toCheck) == 1 { - permSelector.WriteString(" AND action = ?") - permSelectorArgs = append(permSelectorArgs, toCheck[0]) + if !useSelfContainedPermissions { + permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'") + permSelector.WriteString(rolesFilter) + permSelectorArgs = append(permSelectorArgs, params...) + if len(toCheck) == 1 { + permSelector.WriteString(" AND action = ?") + permSelectorArgs = append(permSelectorArgs, toCheck[0]) + } else { + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + permSelectorArgs = append(permSelectorArgs, toCheck...) + permSelectorArgs = append(permSelectorArgs, len(toCheck)) + } } else { - permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") - permSelectorArgs = append(permSelectorArgs, toCheck...) - permSelectorArgs = append(permSelectorArgs, len(toCheck)) + actions := parseStringSliceFromInterfaceSlice(toCheck) + + permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix) + + if len(permSelectorArgs) > 0 { + permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "") + } else { + permSelector.WriteString("(") + } } + permSelector.WriteRune(')') switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) { @@ -271,15 +321,20 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { args = append(args, nestedFoldersArgs...) } default: - builder.WriteString("(dashboard.uid IN ") - builder.WriteString(permSelector.String()) - args = append(args, permSelectorArgs...) + if len(permSelectorArgs) > 0 { + builder.WriteString("(dashboard.uid IN ") + builder.WriteString(permSelector.String()) + args = append(args, permSelectorArgs...) + } else { + builder.WriteString("(1 = 0") + } } builder.WriteString(" AND dashboard.is_folder)") } else { builder.WriteString("dashboard.is_folder") } } + builder.WriteRune(')') f.where = clause{string: builder.String(), params: args} @@ -365,3 +420,36 @@ func nestedFoldersSelectors(permSelector string, permSelectorArgs []interface{}, return strings.Join(wheres, ") OR "), args } + +func parseStringSliceFromInterfaceSlice(slice []interface{}) []string { + result := make([]string, 0, len(slice)) + for _, s := range slice { + result = append(result, s.(string)) + } + return result +} + +func getAllowedUIDs(actions []string, user *user.SignedInUser, scopePrefix string) []interface{} { + uidToActions := make(map[string]map[string]struct{}) + for _, action := range actions { + for _, uidScope := range user.Permissions[user.OrgID][action] { + if !strings.HasPrefix(uidScope, scopePrefix) { + continue + } + uid := strings.TrimPrefix(uidScope, scopePrefix) + if _, exists := uidToActions[uid]; !exists { + uidToActions[uid] = make(map[string]struct{}) + } + uidToActions[uid][action] = struct{}{} + } + } + + // args max capacity is the length of the different uids + args := make([]interface{}, 0, len(uidToActions)) + for uid, assignedActions := range uidToActions { + if len(assignedActions) == len(actions) { + args = append(args, uid) + } + } + return args +} diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 071e6ace713..9d1a22d13af 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -111,6 +112,30 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, expectedResult: 2, }, + { + desc: "Should return the dashboards that the User has dashboards:write permission on in case of 'edit' permission", + permission: dashboards.PERMISSION_EDIT, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:31"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"}, + }, + expectedResult: 1, + }, + { + desc: "Should return the folders that the User has dashboards:create permission on in case of 'edit' permission", + permission: dashboards.PERMISSION_EDIT, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:4"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, + }, + expectedResult: 1, + }, { desc: "Should return folders that users can read alerts from", permission: dashboards.PERMISSION_VIEW, @@ -160,6 +185,167 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { } } +func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + type testCase struct { + desc string + queryType string + permission dashboards.PermissionType + signedInUserPermissions []accesscontrol.Permission + expectedResult int + } + + tests := []testCase{ + { + desc: "Should be able to view all dashboards with wildcard scope", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, + }, + expectedResult: 100, + }, + { + desc: "Should be able to view all dashboards with folder wildcard scope", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, + }, + expectedResult: 100, + }, + { + desc: "Should not be able to view any dashboards or folders without any permissions", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{}, + expectedResult: 0, + }, + { + desc: "Should be able to view a subset of dashboards with dashboard scopes", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:110"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:40"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:22"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:13"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:55"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:99"}, + }, + expectedResult: 6, + }, + { + desc: "Should be able to view a subset of dashboards with dashboard action and folder scope", + permission: dashboards.PERMISSION_VIEW, + + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:8"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:10"}, + }, + expectedResult: 20, + }, + { + desc: "Should be able to view all folders with folder wildcard", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:*"}, + }, + expectedResult: 10, + }, + { + desc: "Should be able to view a subset folders", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:6"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:9"}, + }, + expectedResult: 3, + }, + { + desc: "Should return folders and dashboard with 'edit' permission", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"}, + }, + expectedResult: 2, + }, + { + desc: "Should return the dashboards that the User has dashboards:write permission on in case of 'edit' permission", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:31"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"}, + }, + expectedResult: 1, + }, + { + desc: "Should return the folders that the User has dashboards:create permission on in case of 'edit' permission", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:4"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, + }, + expectedResult: 1, + }, + { + desc: "Should return folders that users can read alerts from", + permission: dashboards.PERMISSION_VIEW, + queryType: searchstore.TypeAlertFolder, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:8"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"}, + }, + expectedResult: 2, + }, + { + desc: "Should return folders that users can read alerts when user has read wildcard", + permission: dashboards.PERMISSION_VIEW, + queryType: searchstore.TypeAlertFolder, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "*"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"}, + }, + expectedResult: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + store := setupTest(t, 10, 100, []accesscontrol.Permission{}) + recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() + require.NoError(t, err) + + usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.signedInUserPermissions)}} + filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, featuremgmt.WithFeatures(), recursiveQueriesAreSupported) + + var result int + err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + q, params := filter.Where() + recQry, recQryParams := filter.With() + params = append(recQryParams, params...) + _, err := sess.SQL(recQry+"\nSELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result) + return err + }) + require.NoError(t, err) + + assert.Equal(t, tt.expectedResult, result) + }) + } +} + func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) { testCases := []struct { desc string @@ -266,6 +452,132 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) { } } +func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermissions(t *testing.T) { + testCases := []struct { + desc string + queryType string + permission dashboards.PermissionType + signedInUserPermissions []accesscontrol.Permission + expectedResult []string + features featuremgmt.FeatureToggles + }{ + { + desc: "Should be able to view dashboards under inherited folders if nested folders are enabled", + queryType: searchstore.TypeDashboard, + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"}, + }, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"}, + }, + { + desc: "Should not be able to view dashboards under inherited folders if nested folders are not enabled", + queryType: searchstore.TypeDashboard, + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"}, + }, + features: featuremgmt.WithFeatures(), + expectedResult: []string{"dashboard under parent folder"}, + }, + { + desc: "Should be able to view inherited folders if nested folders are enabled", + queryType: searchstore.TypeFolder, + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"}, + }, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + expectedResult: []string{"parent", "subfolder"}, + }, + { + desc: "Should not be able to view inherited folders if nested folders are not enabled", + queryType: searchstore.TypeFolder, + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"}, + }, + features: featuremgmt.WithFeatures(), + expectedResult: []string{"parent"}, + }, + { + desc: "Should be able to view inherited dashboards and folders if nested folders are enabled", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"}, + }, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + expectedResult: []string{"parent", "subfolder", "dashboard under parent folder", "dashboard under subfolder"}, + }, + { + desc: "Should not be able to view inherited dashboards and folders if nested folders are not enabled", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"}, + }, + features: featuremgmt.WithFeatures(), + expectedResult: []string{"parent", "dashboard under parent folder"}, + }, + { + desc: "Should be able to edit inherited dashboards and folders if nested folders are enabled", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:subfolder"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:subfolder"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:subfolder"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:subfolder"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:parent"}, + }, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + expectedResult: []string{"subfolder", "dashboard under parent folder", "dashboard under subfolder"}, + }, + } + + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + + var orgID int64 = 1 + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + helperUser := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, + Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{ + { + Action: dashboards.ActionFoldersCreate, + }, + { + Action: dashboards.ActionFoldersWrite, + Scope: dashboards.ScopeFoldersAll, + }, + }), + }, + } + usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.signedInUserPermissions)}} + db := setupNestedTest(t, helperUser, []accesscontrol.Permission{}, orgID, tc.features) + recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported() + require.NoError(t, err) + filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, tc.features, recursiveQueriesAreSupported) + var result []string + err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + q, params := filter.Where() + recQry, recQryParams := filter.With() + params = append(recQryParams, params...) + err := sess.SQL(recQry+"\nSELECT title FROM dashboard WHERE "+q, params...).Find(&result) + return err + }) + require.NoError(t, err) + assert.Equal(t, tc.expectedResult, result) + }) + } +} + func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB { t.Helper() diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index d0ff601382b..e7d16343ef8 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -206,6 +206,7 @@ type SignedInUser struct { Login string Name string Email string + AuthenticatedBy string ApiKeyID int64 `xorm:"api_key_id"` IsServiceAccount bool `xorm:"is_service_account"` OrgCount int