diff --git a/pkg/services/authz/rbac/cache.go b/pkg/services/authz/rbac/cache.go index 7b3b425b24f..49ce54278c0 100644 --- a/pkg/services/authz/rbac/cache.go +++ b/pkg/services/authz/rbac/cache.go @@ -8,6 +8,10 @@ func userIdentifierCacheKeyById(namespace, ID string) string { return "ID_" + namespace + "_" + ID } +func anonymousPermCacheKey(namespace, action string) string { + return namespace + "_anonymous_" + action +} + func userPermCacheKey(namespace, userUID, action string) string { return namespace + "_" + userUID + "_" + action } diff --git a/pkg/services/authz/rbac/models.go b/pkg/services/authz/rbac/models.go index 07c301259e6..e9931b63637 100644 --- a/pkg/services/authz/rbac/models.go +++ b/pkg/services/authz/rbac/models.go @@ -4,6 +4,7 @@ import "github.com/grafana/authlib/claims" type CheckRequest struct { Namespace claims.NamespaceInfo + IdentityType claims.IdentityType UserUID string Action string Group string @@ -14,12 +15,13 @@ type CheckRequest struct { } type ListRequest struct { - Namespace claims.NamespaceInfo - UserUID string - Group string - Resource string - Verb string - Action string + Namespace claims.NamespaceInfo + IdentityType claims.IdentityType + UserUID string + Group string + Resource string + Verb string + Action string } type FolderNode struct { diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 448c8ffd1ea..f15f7d5bf09 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -86,7 +86,7 @@ func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv } ctx = request.WithNamespace(ctx, req.GetNamespace()) - permissions, err := s.getUserPermissions(ctx, checkReq.Namespace, checkReq.UserUID, checkReq.Action) + permissions, err := s.getUserPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) return deny, err @@ -112,7 +112,7 @@ func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1. } ctx = request.WithNamespace(ctx, req.GetNamespace()) - permissions, err := s.getUserPermissions(ctx, listReq.Namespace, listReq.UserUID, listReq.Action) + permissions, err := s.getUserPermissions(ctx, listReq.Namespace, listReq.IdentityType, listReq.UserUID, listReq.Action) if err != nil { ctxLogger.Error("could not get user permissions", "subject", req.GetSubject(), "error", err) return nil, err @@ -127,7 +127,7 @@ func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRe return nil, err } - userUID, err := s.validateSubject(ctx, req.GetSubject()) + userUID, idType, err := s.validateSubject(ctx, req.GetSubject()) if err != nil { return nil, err } @@ -140,6 +140,7 @@ func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRe checkReq := &CheckRequest{ Namespace: ns, UserUID: userUID, + IdentityType: idType, Action: action, Group: req.GetGroup(), Resource: req.GetResource(), @@ -156,7 +157,7 @@ func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequ return nil, err } - userUID, err := s.validateSubject(ctx, req.GetSubject()) + userUID, idType, err := s.validateSubject(ctx, req.GetSubject()) if err != nil { return nil, err } @@ -167,12 +168,13 @@ func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequ } listReq := &ListRequest{ - Namespace: ns, - UserUID: userUID, - Action: action, - Group: req.GetGroup(), - Resource: req.GetResource(), - Verb: req.GetVerb(), + Namespace: ns, + UserUID: userUID, + IdentityType: idType, + Action: action, + Group: req.GetGroup(), + Resource: req.GetResource(), + Verb: req.GetVerb(), } return listReq, nil } @@ -196,21 +198,22 @@ func validateNamespace(ctx context.Context, nameSpace string) (claims.NamespaceI return ns, nil } -func (s *Service) validateSubject(ctx context.Context, subject string) (string, error) { +func (s *Service) validateSubject(ctx context.Context, subject string) (string, claims.IdentityType, error) { if subject == "" { - return "", status.Error(codes.InvalidArgument, "subject is required") + return "", "", status.Error(codes.InvalidArgument, "subject is required") } ctxLogger := s.logger.FromContext(ctx) identityType, userUID, err := claims.ParseTypeID(subject) if err != nil { - return "", err + return "", "", err } - // Permission check currently only checks user and service account permissions, so might return a false negative for other types - if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount) { - ctxLogger.Warn("unsupported identity type", "type", identityType) + // Permission check currently only checks user, anonymous user and service account permissions + if !(identityType == claims.TypeUser || identityType == claims.TypeServiceAccount || identityType == claims.TypeAnonymous) { + ctxLogger.Error("unsupported identity type", "type", identityType) + return "", "", status.Error(codes.PermissionDenied, "unsupported identity type") } - return userUID, nil + return userUID, identityType, nil } func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, error) { @@ -226,7 +229,11 @@ func (s *Service) validateAction(ctx context.Context, group, resource, verb stri return action, nil } -func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInfo, userID, action string) (map[string]bool, error) { +func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInfo, idType claims.IdentityType, userID, action string) (map[string]bool, error) { + if idType == claims.TypeAnonymous { + return s.getAnonymousPermissions(ctx, ns, action) + } + userIdentifiers, err := s.GetUserIdentifiers(ctx, ns, userID) if err != nil { return nil, err @@ -264,6 +271,21 @@ func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInf return scopeMap, nil } +func (s *Service) getAnonymousPermissions(ctx context.Context, ns claims.NamespaceInfo, action string) (map[string]bool, error) { + anonPermKey := anonymousPermCacheKey(ns.Value, action) + if cached, ok := s.permCache.Get(anonPermKey); ok { + return cached.(map[string]bool), nil + } + + permissions, err := s.store.GetUserPermissions(ctx, ns, store.PermissionsQuery{Action: action, Role: "Viewer"}) + if err != nil { + return nil, err + } + scopeMap := getScopeMap(permissions) + s.permCache.Set(anonPermKey, scopeMap, 0) + return scopeMap, nil +} + func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) { uidCacheKey := userIdentifierCacheKey(ns.Value, userUID) if cached, ok := s.idCache.Get(uidCacheKey); ok { diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go index eb4ba1895a6..bc593de0d81 100644 --- a/pkg/services/authz/rbac/service_test.go +++ b/pkg/services/authz/rbac/service_test.go @@ -370,7 +370,7 @@ func TestService_getUserPermissions(t *testing.T) { teamCache: localcache.New(shortCacheTTL, shortCleanupInterval), } - perms, err := s.getUserPermissions(ctx, ns, userID.UID, action) + perms, err := s.getUserPermissions(ctx, ns, claims.TypeUser, userID.UID, action) require.NoError(t, err) require.Len(t, perms, len(tc.expectedPerms)) for _, perm := range tc.permissions { diff --git a/pkg/services/authz/rbac/store/permission_query.sql b/pkg/services/authz/rbac/store/permission_query.sql index da222f09fa0..10e466f8074 100644 --- a/pkg/services/authz/rbac/store/permission_query.sql +++ b/pkg/services/authz/rbac/store/permission_query.sql @@ -1,13 +1,15 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM {{ .Ident .PermissionTable }} as p WHERE p.action = {{ .Arg .Query.Action }} AND p.role_id IN ( + SELECT role_id FROM {{ .Ident .BuiltinRoleTable }} as br WHERE (br.role = {{ .Arg .Query.Role }} AND (br.org_id = {{ .Arg .Query.OrgID }} OR br.org_id = 0)) + {{ if .Query.IsServerAdmin }} + OR (br.role = 'Grafana Admin') + {{ end }} + {{ if .Query.UserID }} + UNION SELECT role_id FROM {{ .Ident .UserRoleTable }} as ur WHERE ur.user_id = {{ .Arg .Query.UserID }} AND (ur.org_id = {{ .Arg .Query.OrgID }} OR ur.org_id = 0) - {{ if .Query.TeamIDs }} + {{ end }} + {{ if .Query.TeamIDs }} UNION SELECT role_id FROM {{ .Ident .TeamRoleTable }} as tr WHERE tr.team_id IN ({{ .ArgList .Query.TeamIDs }}) AND tr.org_id = {{ .Arg .Query.OrgID }} {{ end }} - UNION ALL - SELECT role_id FROM {{ .Ident .BuiltinRoleTable }} as br WHERE (br.role = {{ .Arg .Query.Role }} AND (br.org_id = {{ .Arg .Query.OrgID }} OR br.org_id = 0)) - {{ if .Query.IsServerAdmin }} - OR (br.role = 'Grafana Admin') - {{ end }} ) diff --git a/pkg/services/authz/rbac/store/sql_test.go b/pkg/services/authz/rbac/store/sql_test.go index c3227179967..37aca917d10 100644 --- a/pkg/services/authz/rbac/store/sql_test.go +++ b/pkg/services/authz/rbac/store/sql_test.go @@ -97,6 +97,14 @@ func TestIdentityQueries(t *testing.T) { TeamIDs: []int64{1, 2}, }), }, + { + Name: "anonymous_user", + Data: getPermissions(&PermissionsQuery{ + OrgID: 1, + Action: "folders:read", + Role: "Viewer", + }), + }, }, sqlFolders: { { diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql index 6af62ecd77b..a38590186d0 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-admin_user.sql @@ -1,7 +1,7 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p WHERE p.action = 'folders:read' AND p.role_id IN ( - SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION ALL SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) - OR (br.role = 'Grafana Admin') + OR (br.role = 'Grafana Admin') + UNION + SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) ) diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-anonymous_user.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-anonymous_user.sql new file mode 100755 index 00000000000..c0a0434e9fc --- /dev/null +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-anonymous_user.sql @@ -0,0 +1,4 @@ +SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p +WHERE p.action = 'folders:read' AND p.role_id IN ( + SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) +) diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql index 568cd54599f..09c374c4000 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-user_with_teams.sql @@ -1,8 +1,8 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p WHERE p.action = 'folders:read' AND p.role_id IN ( + SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) + UNION SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) UNION SELECT role_id FROM `grafana`.`team_role` as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 - UNION ALL - SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) ) diff --git a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql index 5f91c914e94..0bf68123439 100755 --- a/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql +++ b/pkg/services/authz/rbac/store/testdata/mysql--permission_query-viewer_user.sql @@ -1,6 +1,6 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM `grafana`.`permission` as p WHERE p.action = 'folders:read' AND p.role_id IN ( - SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION ALL SELECT role_id FROM `grafana`.`builtin_role` as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION + SELECT role_id FROM `grafana`.`user_role` as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) ) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql index e210f12b984..3e4ac0a954c 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-admin_user.sql @@ -1,7 +1,7 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p WHERE p.action = 'folders:read' AND p.role_id IN ( - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION ALL SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) - OR (br.role = 'Grafana Admin') + OR (br.role = 'Grafana Admin') + UNION + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) ) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-anonymous_user.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-anonymous_user.sql new file mode 100755 index 00000000000..d8a4944a45f --- /dev/null +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-anonymous_user.sql @@ -0,0 +1,4 @@ +SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +WHERE p.action = 'folders:read' AND p.role_id IN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) +) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql index e53983aabea..d7b8ea9ed0d 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-user_with_teams.sql @@ -1,8 +1,8 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p WHERE p.action = 'folders:read' AND p.role_id IN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) + UNION SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) UNION SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 - UNION ALL - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) ) diff --git a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql index 5b3d5e5c583..71e2412fbc1 100755 --- a/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql +++ b/pkg/services/authz/rbac/store/testdata/postgres--permission_query-viewer_user.sql @@ -1,6 +1,6 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p WHERE p.action = 'folders:read' AND p.role_id IN ( - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION ALL SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) ) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql index e210f12b984..3e4ac0a954c 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-admin_user.sql @@ -1,7 +1,7 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p WHERE p.action = 'folders:read' AND p.role_id IN ( - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION ALL SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Admin' AND (br.org_id = 1 OR br.org_id = 0)) - OR (br.role = 'Grafana Admin') + OR (br.role = 'Grafana Admin') + UNION + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) ) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-anonymous_user.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-anonymous_user.sql new file mode 100755 index 00000000000..d8a4944a45f --- /dev/null +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-anonymous_user.sql @@ -0,0 +1,4 @@ +SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p +WHERE p.action = 'folders:read' AND p.role_id IN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) +) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql index e53983aabea..d7b8ea9ed0d 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-user_with_teams.sql @@ -1,8 +1,8 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p WHERE p.action = 'folders:read' AND p.role_id IN ( + SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) + UNION SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) UNION SELECT role_id FROM "grafana"."team_role" as tr WHERE tr.team_id IN (1, 2) AND tr.org_id = 1 - UNION ALL - SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'None' AND (br.org_id = 1 OR br.org_id = 0)) ) diff --git a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql index 5b3d5e5c583..71e2412fbc1 100755 --- a/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql +++ b/pkg/services/authz/rbac/store/testdata/sqlite--permission_query-viewer_user.sql @@ -1,6 +1,6 @@ SELECT p.action, p.kind, p.attribute, p.identifier, p.scope FROM "grafana"."permission" as p WHERE p.action = 'folders:read' AND p.role_id IN ( - SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) - UNION ALL SELECT role_id FROM "grafana"."builtin_role" as br WHERE (br.role = 'Viewer' AND (br.org_id = 1 OR br.org_id = 0)) + UNION + SELECT role_id FROM "grafana"."user_role" as ur WHERE ur.user_id = 1 AND (ur.org_id = 1 OR ur.org_id = 0) )