diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index a24a592e72d..17e72b61182 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -625,7 +625,9 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.UseMiddleware(hs.ContextHandler.Middleware) m.Use(middleware.OrgRedirect(hs.Cfg, hs.userService)) - m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService)) + if !hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService)) + } // needs to be after context handler if hs.Cfg.EnforceDomain { diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 0d092d2f583..63eac98ee18 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -57,6 +57,8 @@ type ClientParams struct { CacheAuthProxyKey string // LookUpParams are the arguments used to look up the entity in the DB. LookUpParams login.UserLookupParams + // SyncPermissions ensure that permissions are loaded from DB and added to the identity + SyncPermissions bool } type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error @@ -221,6 +223,8 @@ type Identity struct { // ClientParams are hints for the auth service on how to handle the identity. // Set by the authenticating client. ClientParams ClientParams + // Permissions is the list of permissions the entity has. + Permissions map[int64]map[string][]string } // Role returns the role of the identity in the active organization. @@ -273,6 +277,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser { HelpFlags1: i.HelpFlags1, LastSeenAt: i.LastSeenAt, Teams: i.Teams, + Permissions: i.Permissions, } namespace, id := i.NamespacedID() @@ -320,6 +325,7 @@ func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientPa LastSeenAt: usr.LastSeenAt, Teams: usr.Teams, ClientParams: params, + Permissions: usr.Permissions, } } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index a591b02f3fd..6761db3887a 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -155,6 +155,7 @@ func ProvideService( } s.RegisterPostAuthHook(userSyncService.FetchSyncedUserHook, 100) + s.RegisterPostAuthHook(sync.ProvidePermissionsSync(accessControlService).SyncPermissionsHook, 110) return s } diff --git a/pkg/services/authn/authnimpl/sync/permission_sync.go b/pkg/services/authn/authnimpl/sync/permission_sync.go new file mode 100644 index 00000000000..06b20270c52 --- /dev/null +++ b/pkg/services/authn/authnimpl/sync/permission_sync.go @@ -0,0 +1,45 @@ +package sync + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/util/errutil" +) + +var ( + errSyncPermissionsForbidden = errutil.NewBase(errutil.StatusForbidden, "permissions.sync.forbidden") +) + +func ProvidePermissionsSync(acService accesscontrol.Service) *PermissionsSync { + return &PermissionsSync{ + ac: acService, + log: log.New("permissions.sync"), + } +} + +type PermissionsSync struct { + ac accesscontrol.Service + log log.Logger +} + +func (s *PermissionsSync) SyncPermissionsHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + if s.ac.IsDisabled() || !identity.ClientParams.SyncPermissions { + return nil + } + + permissions, err := s.ac.GetUserPermissions(ctx, identity.SignedInUser(), + accesscontrol.Options{ReloadCache: false}) + if err != nil { + s.log.FromContext(ctx).Error("failed to fetch permissions from db", "error", err, "user_id", identity.ID) + return errSyncPermissionsForbidden + } + + if identity.Permissions == nil { + identity.Permissions = make(map[int64]map[string][]string) + } + identity.Permissions[identity.OrgID] = accesscontrol.GroupScopesByAction(permissions) + return nil +} diff --git a/pkg/services/authn/authnimpl/sync/permission_sync_test.go b/pkg/services/authn/authnimpl/sync/permission_sync_test.go new file mode 100644 index 00000000000..c9744ebd8ff --- /dev/null +++ b/pkg/services/authn/authnimpl/sync/permission_sync_test.go @@ -0,0 +1,81 @@ +package sync + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPermissionsSync_SyncPermission(t *testing.T) { + type testCase struct { + name string + identity *authn.Identity + rbacDisabled bool + expectedPermissions []accesscontrol.Permission + } + testCases := []testCase{ + { + name: "enriches the identity successfully when SyncPermissions is true", + identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + rbacDisabled: false, + expectedPermissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionUsersRead}, + }, + }, + { + name: "does not load the permissions when SyncPermissions is false", + identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + rbacDisabled: false, + expectedPermissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionUsersRead}, + }, + }, + { + name: "does not load the permissions when RBAC is disabled", + rbacDisabled: true, + identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + expectedPermissions: []accesscontrol.Permission{}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + s := setupTestEnv(tt.rbacDisabled) + + err := s.SyncPermissionsHook(context.Background(), tt.identity, &authn.Request{}) + require.NoError(t, err) + + if !tt.rbacDisabled { + assert.Equal(t, 1, len(tt.identity.Permissions)) + assert.Equal(t, accesscontrol.GroupScopesByAction(tt.expectedPermissions), tt.identity.Permissions[tt.identity.OrgID]) + } else { + assert.Equal(t, 0, len(tt.identity.Permissions)) + } + }) + } +} + +func setupTestEnv(rbacDisabled bool) *PermissionsSync { + acMock := &acmock.Mock{ + IsDisabledFunc: func() bool { + return rbacDisabled + }, + GetUserPermissionsFunc: func(ctx context.Context, siu *user.SignedInUser, o accesscontrol.Options) ([]accesscontrol.Permission, error) { + return []accesscontrol.Permission{ + {Action: accesscontrol.ActionUsersRead}, + }, nil + }, + } + s := &PermissionsSync{ + ac: acMock, + log: log.NewNopLogger(), + } + return s +} diff --git a/pkg/services/authn/clients/anonymous.go b/pkg/services/authn/clients/anonymous.go index 2d5d919954e..bd29a1eab04 100644 --- a/pkg/services/authn/clients/anonymous.go +++ b/pkg/services/authn/clients/anonymous.go @@ -56,7 +56,7 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn. OrgID: o.ID, OrgName: o.Name, OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)}, - ClientParams: authn.ClientParams{}, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, nil } diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index 1b712f2fe2f..52da3782397 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -64,9 +64,10 @@ 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}, + ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID), + OrgID: apiKey.OrgID, + OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role}, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, nil } @@ -79,7 +80,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{}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), 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 79d98df756c..2ce35f1eab9 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -52,6 +52,9 @@ func TestAPIKey_Authenticate(t *testing.T) { ID: "api-key:1", OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, }, }, { @@ -82,6 +85,9 @@ func TestAPIKey_Authenticate(t *testing.T) { Name: "test", OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, IsGrafanaAdmin: boolPtr(false), + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, }, }, { diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index af77b62ae84..7919fb82b9a 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -108,7 +108,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{}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}), 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 b33a00fd5a0..f2cd1503345 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -142,7 +142,12 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { password: "password", 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)}, + expectedIdentity: &authn.Identity{ + ID: "user:1", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: "Viewer"}, + IsGrafanaAdmin: boolPtr(false), + ClientParams: authn.ClientParams{SyncPermissions: true}}, }, { desc: "should fail for incorrect password", diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index fa0a2ebab5e..0ef0fdc14c1 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -70,6 +70,7 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi ClientParams: authn.ClientParams{ SyncUser: true, FetchSyncedUser: true, + SyncPermissions: true, SyncOrgRoles: !s.cfg.JWTAuthSkipOrgRoleSync, AllowSignUp: s.cfg.JWTAuthAutoSignUp, }} diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 6eb6d555a1c..04a4c837bfe 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -53,6 +53,7 @@ func TestAuthenticateJWT(t *testing.T) { AllowSignUp: true, FetchSyncedUser: true, SyncOrgRoles: true, + SyncPermissions: true, LookUpParams: login.UserLookupParams{ UserID: nil, Email: stringPtr("eai.doe@cor.po"), diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index c2e166bdfcd..76013eb9c34 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -85,6 +85,7 @@ func (c *LDAP) identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo) * SyncTeams: true, EnableDisabledUsers: true, FetchSyncedUser: true, + SyncPermissions: true, SyncOrgRoles: !c.cfg.LDAPSkipOrgRoleSync, AllowSignUp: c.cfg.LDAPAllowSignup, LookUpParams: login.UserLookupParams{ diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index 0f7a0ace532..0650550ecb8 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -53,6 +53,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { EnableDisabledUsers: true, FetchSyncedUser: true, SyncOrgRoles: true, + SyncPermissions: true, LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), @@ -118,6 +119,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { EnableDisabledUsers: true, FetchSyncedUser: true, SyncOrgRoles: true, + SyncPermissions: true, LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 906c29a0dee..e6f4eeb95c6 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -154,6 +154,7 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden SyncUser: true, SyncTeams: true, FetchSyncedUser: true, + SyncPermissions: true, AllowSignUp: c.connector.IsSignupAllowed(), // skip org role flag is checked and handled in the connector. For now we can skip the hook if no roles are passed SyncOrgRoles: len(orgRoles) > 0, diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index dc1554c0c73..d4683b4fc81 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -96,7 +96,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{}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), nil } } } diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index 52e39b1a981..23affc2c978 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -45,9 +45,10 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide var identity *authn.Identity if renderUsr.UserID <= 0 { identity = &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 0), - OrgID: renderUsr.OrgID, - OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, + ID: authn.NamespacedID(authn.NamespaceUser, 0), + OrgID: renderUsr.OrgID, + OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, + ClientParams: authn.ClientParams{SyncPermissions: true}, } } else { usr, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{UserID: renderUsr.UserID, OrgID: renderUsr.OrgID}) @@ -55,7 +56,7 @@ 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{}) + identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}) } identity.LastSeenAt = time.Now() diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go index d38e2421634..4a1a9728948 100644 --- a/pkg/services/authn/clients/render_test.go +++ b/pkg/services/authn/clients/render_test.go @@ -38,10 +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, + ID: "user:0", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + AuthModule: login.RenderModule, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, @@ -64,6 +65,7 @@ func TestRender_Authenticate(t *testing.T) { OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, IsGrafanaAdmin: boolPtr(false), AuthModule: login.RenderModule, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, diff --git a/pkg/services/authn/clients/session.go b/pkg/services/authn/clients/session.go index 5a2ece98854..071ab01ad25 100644 --- a/pkg/services/authn/clients/session.go +++ b/pkg/services/authn/clients/session.go @@ -62,7 +62,7 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id return nil, err } - identity := authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{}) + identity := authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}) identity.SessionToken = token return identity, nil diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index bd640ff9837..f19973eda7e 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -108,6 +108,9 @@ func TestSession_Authenticate(t *testing.T) { OrgID: 1, OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleEditor}, IsGrafanaAdmin: boolPtr(false), + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, }, wantErr: false, },