diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index d8bc38f838f..a763de15613 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -59,6 +59,8 @@ type Service interface { DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error // SyncUserRoles adds provided roles to user SyncUserRoles(ctx context.Context, orgID int64, cmd SyncUserRolesCommand) error + // GetStaicRoles returns a map where key organization role and value is a static rbac role. + GetStaticRoles(ctx context.Context) map[string]*RoleDTO } //go:generate mockery --name Store --structname MockStore --outpkg actest --filename store_mock.go --output ./actest/ diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 3a13f94af69..a79e30343cc 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -464,7 +464,15 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error { s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) { if basicRole, ok := s.roles[br]; ok { - basicRole.Permissions = append(basicRole.Permissions, registration.Role.Permissions...) + for _, p := range registration.Role.Permissions { + perm := accesscontrol.Permission{ + Action: p.Action, + Scope: p.Scope, + } + + perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) + basicRole.Permissions = append(basicRole.Permissions, perm) + } } else { s.log.Error("Unknown builtin role", "builtInRole", br) } @@ -770,10 +778,14 @@ func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalService return s.store.DeleteExternalServiceRole(ctx, slug) } -func (*Service) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontrol.SyncUserRolesCommand) error { +func (s *Service) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontrol.SyncUserRolesCommand) error { return nil } +func (s *Service) GetStaticRoles(ctx context.Context) map[string]*accesscontrol.RoleDTO { + return s.roles +} + func (s *Service) GetRoleByName(ctx context.Context, orgID int64, roleName string) (*accesscontrol.RoleDTO, error) { _, span := tracer.Start(ctx, "accesscontrol.acimpl.GetRoleByName") defer span.End() diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index 674fb544775..9b1d04ecd96 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -373,6 +373,7 @@ func TestService_RegisterFixedRoles(t *testing.T) { builtinRole, ok := ac.roles[br] assert.True(t, ok) for _, expectedPermission := range registration.Role.Permissions { + expectedPermission.Kind, expectedPermission.Attribute, expectedPermission.Identifier = accesscontrol.SplitScope(expectedPermission.Scope) assert.Contains(t, builtinRole.Permissions, expectedPermission) } } diff --git a/pkg/services/accesscontrol/mock/mock.go b/pkg/services/accesscontrol/mock/mock.go index 146e1d912a9..2059a46de5f 100644 --- a/pkg/services/accesscontrol/mock/mock.go +++ b/pkg/services/accesscontrol/mock/mock.go @@ -38,6 +38,9 @@ type Calls struct { } type Mock struct { + accesscontrol.Service + accesscontrol.AccessControl + // Unless an override is provided, permissions will be returned by GetUserPermissions permissions []accesscontrol.Permission // Unless an override is provided, builtInRoles will be returned by GetUserBuiltInRoles diff --git a/pkg/services/authz/client.go b/pkg/services/authz/client.go index c3399811a00..66f3136445a 100644 --- a/pkg/services/authz/client.go +++ b/pkg/services/authz/client.go @@ -18,7 +18,9 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authz/rbac" + "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/setting" @@ -30,8 +32,12 @@ const authzServiceAudience = "authzService" // ProvideAuthZClient provides an AuthZ client and creates the AuthZ service. func ProvideAuthZClient( - cfg *setting.Cfg, features featuremgmt.FeatureToggles, grpcServer grpcserver.Provider, - tracer tracing.Tracer, db db.DB, + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, + grpcServer grpcserver.Provider, + tracer tracing.Tracer, + db db.DB, + acService accesscontrol.Service, ) (authzlib.AccessClient, error) { authCfg, err := ReadCfg(cfg) if err != nil { @@ -43,16 +49,25 @@ func ProvideAuthZClient( return nil, errors.New("authZGRPCServer feature toggle is required for cloud and grpc mode") } - // Register the server - sql := legacysql.NewDatabaseProvider(db) - server := rbac.NewService(sql, legacy.NewLegacySQLStores(sql), log.New("authz-grpc-server"), tracer) - switch authCfg.mode { case ModeGRPC: return newGrpcLegacyClient(authCfg, tracer) case ModeCloud: return newCloudLegacyClient(authCfg, tracer) default: + sql := legacysql.NewDatabaseProvider(db) + + // Register the server + server := rbac.NewService( + sql, + legacy.NewLegacySQLStores(sql), + store.NewUnionPermissionStore( + store.NewStaticPermissionStore(acService), + store.NewSQLPermissionStore(sql, tracer), + ), + log.New("authz-grpc-server"), + tracer, + ) return newInProcLegacyClient(server, tracer) } } diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 8834b3e1da9..3c20ce8f96b 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -40,9 +40,10 @@ type Service struct { authzv1.UnimplementedAuthzServiceServer authzextv1.UnimplementedAuthzExtentionServiceServer - store store.Store - identityStore legacy.LegacyIdentityStore - actionMapper *mappers.K8sRbacMapper + store store.Store + permissionStore store.PermissionStore + identityStore legacy.LegacyIdentityStore + actionMapper *mappers.K8sRbacMapper logger log.Logger tracer tracing.Tracer @@ -58,19 +59,27 @@ type Service struct { sf *singleflight.Group } -func NewService(sql legacysql.LegacyDatabaseProvider, identityStore legacy.LegacyIdentityStore, logger log.Logger, tracer tracing.Tracer) *Service { +func NewService( + sql legacysql.LegacyDatabaseProvider, + identityStore legacy.LegacyIdentityStore, + permissionStore store.PermissionStore, + logger log.Logger, + tracer tracing.Tracer, + +) *Service { return &Service{ - store: store.NewStore(sql, tracer), - identityStore: identityStore, - actionMapper: mappers.NewK8sRbacMapper(), - logger: logger, - tracer: tracer, - idCache: localcache.New(longCacheTTL, longCleanupInterval), - permCache: localcache.New(shortCacheTTL, shortCleanupInterval), - teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), - basicRoleCache: localcache.New(longCacheTTL, longCleanupInterval), - folderCache: localcache.New(shortCacheTTL, shortCleanupInterval), - sf: new(singleflight.Group), + store: store.NewStore(sql, tracer), + permissionStore: permissionStore, + identityStore: identityStore, + actionMapper: mappers.NewK8sRbacMapper(), + logger: logger, + tracer: tracer, + idCache: localcache.New(longCacheTTL, longCleanupInterval), + permCache: localcache.New(shortCacheTTL, shortCleanupInterval), + teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), + basicRoleCache: localcache.New(longCacheTTL, longCleanupInterval), + folderCache: localcache.New(shortCacheTTL, shortCleanupInterval), + sf: new(singleflight.Group), } } @@ -282,7 +291,7 @@ func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInf IsServerAdmin: basicRoles.IsAdmin, } - permissions, err := s.store.GetUserPermissions(ctx, ns, userPermQuery) + permissions, err := s.permissionStore.GetUserPermissions(ctx, ns, userPermQuery) if err != nil { return nil, err } @@ -311,7 +320,7 @@ func (s *Service) getAnonymousPermissions(ctx context.Context, ns claims.Namespa } res, err, _ := s.sf.Do(anonPermKey+"_getAnonymousPermissions", func() (interface{}, error) { - permissions, err := s.store.GetUserPermissions(ctx, ns, store.PermissionsQuery{Action: action, ActionSets: actionSets, Role: "Viewer"}) + permissions, err := s.permissionStore.GetUserPermissions(ctx, ns, store.PermissionsQuery{Action: action, ActionSets: actionSets, Role: "Viewer"}) if err != nil { return nil, err } diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index 8d10c1693f2..2825fa76acc 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -286,10 +286,11 @@ func TestService_getUserBasicRole(t *testing.T) { } s := &Service{ - basicRoleCache: cacheService, - store: store, - logger: log.New("test"), - tracer: tracing.NewNoopTracerService(), + basicRoleCache: cacheService, + store: store, + permissionStore: store, + logger: log.New("test"), + tracer: tracing.NewNoopTracerService(), } role, err := s.getUserBasicRole(ctx, ns, userIdentifiers) @@ -361,16 +362,17 @@ func TestService_getUserPermissions(t *testing.T) { } s := &Service{ - store: store, - identityStore: &fakeIdentityStore{teams: []int64{1, 2}}, - actionMapper: mappers.NewK8sRbacMapper(), - logger: log.New("test"), - tracer: tracing.NewNoopTracerService(), - idCache: localcache.New(longCacheTTL, longCleanupInterval), - permCache: cacheService, - sf: new(singleflight.Group), - basicRoleCache: localcache.New(longCacheTTL, longCleanupInterval), - teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), + store: store, + permissionStore: store, + identityStore: &fakeIdentityStore{teams: []int64{1, 2}}, + actionMapper: mappers.NewK8sRbacMapper(), + logger: log.New("test"), + tracer: tracing.NewNoopTracerService(), + idCache: localcache.New(longCacheTTL, longCleanupInterval), + permCache: cacheService, + sf: new(singleflight.Group), + basicRoleCache: localcache.New(longCacheTTL, longCleanupInterval), + teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), } perms, err := s.getUserPermissions(ctx, ns, claims.TypeUser, userID.UID, action) @@ -437,11 +439,12 @@ func TestService_buildFolderTree(t *testing.T) { store := &fakeStore{folders: tc.folders} s := &Service{ - store: store, - folderCache: cacheService, - logger: log.New("test"), - sf: new(singleflight.Group), - tracer: tracing.NewNoopTracerService(), + store: store, + permissionStore: store, + folderCache: cacheService, + logger: log.New("test"), + sf: new(singleflight.Group), + tracer: tracing.NewNoopTracerService(), } tree, err := s.buildFolderTree(ctx, ns) diff --git a/pkg/services/authz/rbac/store/models.go b/pkg/services/authz/rbac/store/models.go index 5df60d261a7..9abcbb8a257 100644 --- a/pkg/services/authz/rbac/store/models.go +++ b/pkg/services/authz/rbac/store/models.go @@ -10,16 +10,6 @@ type BasicRole struct { IsAdmin bool } -type PermissionsQuery struct { - OrgID int64 - UserID int64 - Action string - ActionSets []string - TeamIDs []int64 - Role string - IsServerAdmin bool -} - type BasicRoleQuery struct { UserID int64 OrgID int64 diff --git a/pkg/services/authz/rbac/store/permission_store.go b/pkg/services/authz/rbac/store/permission_store.go new file mode 100644 index 00000000000..bd08b9b31a1 --- /dev/null +++ b/pkg/services/authz/rbac/store/permission_store.go @@ -0,0 +1,159 @@ +package store + +import ( + "context" + + "github.com/grafana/authlib/claims" + + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +type PermissionStore interface { + GetUserPermissions(ctx context.Context, ns claims.NamespaceInfo, query PermissionsQuery) ([]accesscontrol.Permission, error) +} + +type PermissionsQuery struct { + OrgID int64 + UserID int64 + Action string + ActionSets []string + TeamIDs []int64 + Role string + IsServerAdmin bool +} + +func NewSQLPermissionStore(sql legacysql.LegacyDatabaseProvider, tracer tracing.Tracer) *SQLPermissionsStore { + return &SQLPermissionsStore{sql, tracer} +} + +var _ PermissionStore = (*SQLPermissionsStore)(nil) + +type SQLPermissionsStore struct { + sql legacysql.LegacyDatabaseProvider + tracer tracing.Tracer +} + +var sqlUserPerms = mustTemplate("permission_query.sql") + +type getPermissionsQuery struct { + sqltemplate.SQLTemplate + Query *PermissionsQuery + + PermissionTable string + UserRoleTable string + TeamRoleTable string + BuiltinRoleTable string +} + +func (r getPermissionsQuery) Validate() error { + return nil +} + +func newGetPermissions(sql *legacysql.LegacyDatabaseHelper, q *PermissionsQuery) getPermissionsQuery { + return getPermissionsQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + Query: q, + PermissionTable: sql.Table("permission"), + UserRoleTable: sql.Table("user_role"), + TeamRoleTable: sql.Table("team_role"), + BuiltinRoleTable: sql.Table("builtin_role"), + } +} + +func (s *SQLPermissionsStore) GetUserPermissions(ctx context.Context, ns claims.NamespaceInfo, query PermissionsQuery) ([]accesscontrol.Permission, error) { + ctx, span := s.tracer.Start(ctx, "authz_direct_db.database.GetUserPermissions") + defer span.End() + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + query.OrgID = ns.OrgID + req := newGetPermissions(sql, &query) + q, err := sqltemplate.Execute(sqlUserPerms, req) + if err != nil { + return nil, err + } + + res, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + if err != nil { + return nil, err + } + defer func() { + if res != nil { + _ = res.Close() + } + }() + + var perms []accesscontrol.Permission + for res.Next() { + var perm accesscontrol.Permission + if err := res.Scan(&perm.Kind, &perm.Attribute, &perm.Identifier, &perm.Scope); err != nil { + return nil, err + } + perms = append(perms, perm) + } + + return perms, nil +} + +var _ PermissionStore = (*StaticPermissionStore)(nil) + +func NewStaticPermissionStore(ac accesscontrol.Service) *StaticPermissionStore { + return &StaticPermissionStore{ac} +} + +type StaticPermissionStore struct { + ac accesscontrol.Service +} + +func (s *StaticPermissionStore) GetUserPermissions(ctx context.Context, ns claims.NamespaceInfo, query PermissionsQuery) ([]accesscontrol.Permission, error) { + roles := []string{query.Role} + if query.IsServerAdmin { + roles = append(roles, "Grafana Admin") + } + + static := s.ac.GetStaticRoles(ctx) + + var permissions []accesscontrol.Permission + for _, name := range roles { + r, ok := static[name] + if !ok { + continue + } + + for _, p := range r.Permissions { + if p.Action == query.Action { + permissions = append(permissions, p) + } + } + } + + return permissions, nil +} + +var _ PermissionStore = (*UnionPermissionStore)(nil) + +func NewUnionPermissionStore(stores ...PermissionStore) *UnionPermissionStore { + return &UnionPermissionStore{stores} +} + +type UnionPermissionStore struct { + stores []PermissionStore +} + +func (u *UnionPermissionStore) GetUserPermissions(ctx context.Context, ns claims.NamespaceInfo, query PermissionsQuery) ([]accesscontrol.Permission, error) { + var permissions []accesscontrol.Permission + for _, s := range u.stores { + result, err := s.GetUserPermissions(ctx, ns, query) + if err != nil { + return nil, err + } + permissions = append(permissions, result...) + } + return permissions, nil +} diff --git a/pkg/services/authz/rbac/store/queries.go b/pkg/services/authz/rbac/store/queries.go index 4dada84ae87..4ad0a3d5e11 100644 --- a/pkg/services/authz/rbac/store/queries.go +++ b/pkg/services/authz/rbac/store/queries.go @@ -14,7 +14,6 @@ var ( sqlTemplatesFS embed.FS sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`)) - sqlUserPerms = mustTemplate("permission_query.sql") sqlQueryBasicRoles = mustTemplate("basic_role_query.sql") sqlUserIdentifiers = mustTemplate("user_identifier_query.sql") sqlFolders = mustTemplate("folder_query.sql") @@ -67,31 +66,6 @@ func newGetBasicRoles(sql *legacysql.LegacyDatabaseHelper, q *BasicRoleQuery) ge } } -type getPermissionsQuery struct { - sqltemplate.SQLTemplate - Query *PermissionsQuery - - PermissionTable string - UserRoleTable string - TeamRoleTable string - BuiltinRoleTable string -} - -func (r getPermissionsQuery) Validate() error { - return nil -} - -func newGetPermissions(sql *legacysql.LegacyDatabaseHelper, q *PermissionsQuery) getPermissionsQuery { - return getPermissionsQuery{ - SQLTemplate: sqltemplate.New(sql.DialectForDriver()), - Query: q, - PermissionTable: sql.Table("permission"), - UserRoleTable: sql.Table("user_role"), - TeamRoleTable: sql.Table("team_role"), - BuiltinRoleTable: sql.Table("builtin_role"), - } -} - type getFoldersQuery struct { sqltemplate.SQLTemplate Query *FolderQuery diff --git a/pkg/services/authz/rbac/store/store.go b/pkg/services/authz/rbac/store/store.go index 510619c2d39..7d8d6d7188c 100644 --- a/pkg/services/authz/rbac/store/store.go +++ b/pkg/services/authz/rbac/store/store.go @@ -7,13 +7,11 @@ import ( "golang.org/x/net/context" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) type Store interface { - GetUserPermissions(ctx context.Context, ns claims.NamespaceInfo, query PermissionsQuery) ([]accesscontrol.Permission, error) GetUserIdentifiers(ctx context.Context, query UserIdentifierQuery) (*UserIdentifiers, error) GetBasicRoles(ctx context.Context, ns claims.NamespaceInfo, query BasicRoleQuery) (*BasicRole, error) GetFolders(ctx context.Context, ns claims.NamespaceInfo) ([]Folder, error) @@ -31,44 +29,6 @@ func NewStore(sql legacysql.LegacyDatabaseProvider, tracer tracing.Tracer) *Stor } } -func (s *StoreImpl) GetUserPermissions(ctx context.Context, ns claims.NamespaceInfo, query PermissionsQuery) ([]accesscontrol.Permission, error) { - ctx, span := s.tracer.Start(ctx, "authz_direct_db.database.GetUserPermissions") - defer span.End() - - sql, err := s.sql(ctx) - if err != nil { - return nil, err - } - - query.OrgID = ns.OrgID - req := newGetPermissions(sql, &query) - q, err := sqltemplate.Execute(sqlUserPerms, req) - if err != nil { - return nil, err - } - - res, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) - if err != nil { - return nil, err - } - defer func() { - if res != nil { - _ = res.Close() - } - }() - - var perms []accesscontrol.Permission - for res.Next() { - var perm accesscontrol.Permission - if err := res.Scan(&perm.Kind, &perm.Attribute, &perm.Identifier, &perm.Scope); err != nil { - return nil, err - } - perms = append(perms, perm) - } - - return perms, nil -} - func (s *StoreImpl) GetUserIdentifiers(ctx context.Context, query UserIdentifierQuery) (*UserIdentifiers, error) { ctx, span := s.tracer.Start(ctx, "authz_direct_db.database.GetUserIdentifiers") defer span.End() diff --git a/pkg/services/authz/server.go b/pkg/services/authz/server.go index 274dc61976c..49f583386fc 100644 --- a/pkg/services/authz/server.go +++ b/pkg/services/authz/server.go @@ -8,12 +8,19 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/rbac" + "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/storage/legacysql" ) func RegisterRBACAuthZService(handler grpcserver.Provider, db legacysql.LegacyDatabaseProvider, tracer tracing.Tracer) { - server := rbac.NewService(db, legacy.NewLegacySQLStores(db), log.New("authz-grpc-server"), tracer) + server := rbac.NewService( + db, + legacy.NewLegacySQLStores(db), + store.NewSQLPermissionStore(db, tracer), + log.New("authz-grpc-server"), + tracer, + ) srv := handler.GetServer() authzv1.RegisterAuthzServiceServer(srv, server) diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 13ccc13d2e6..7d1d3084e81 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -442,14 +442,7 @@ func (c *K8sTestHelper) LoadYAMLOrJSON(body string) *unstructured.Unstructured { func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers { c.t.Helper() users := OrgUsers{ - Admin: c.CreateUser("admin", orgName, org.RoleAdmin, []resourcepermissions.SetResourcePermissionCommand{ - { - Actions: []string{"dashboards:read", "dashboards:write", "dashboards:create", "dashboards:delete"}, - Resource: "dashboards", - ResourceAttribute: "uid", - ResourceID: "*", - }, - }), + Admin: c.CreateUser("admin", orgName, org.RoleAdmin, nil), Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil), Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil), }