package rbac import ( "context" "fmt" "strconv" "strings" "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" ) type ScopeResolverFunc func(scope string) (string, error) func (s *Service) fetchServiceAccounts(ctx context.Context, ns types.NamespaceInfo) (map[int64]string, error) { serviceAccounts, err := s.identityStore.ListServiceAccounts(ctx, ns, legacy.ListServiceAccountsQuery{}) if err != nil { return nil, fmt.Errorf("could not fetch service accounts: %w", err) } saIDs := make(map[int64]string, len(serviceAccounts.Items)) for _, sa := range serviceAccounts.Items { saIDs[sa.ID] = sa.UID } return saIDs, nil } // Should return an error if we fail to build the resolver. func (s *Service) newServiceAccountNameResolver(ctx context.Context, ns types.NamespaceInfo) (ScopeResolverFunc, error) { return func(scope string) (string, error) { saIDs, err := s.fetchServiceAccounts(ctx, ns) if err != nil { return "", fmt.Errorf("could not build resolver: %w", err) } serviceAccountIDStr := strings.TrimPrefix(scope, "serviceaccounts:id:") if serviceAccountIDStr == "" { return "", fmt.Errorf("service account ID is empty") } if serviceAccountIDStr == "*" { return "serviceaccounts:uid:*", nil } serviceAccountID, err := strconv.ParseInt(serviceAccountIDStr, 10, 64) if err != nil { return "", fmt.Errorf("invalid service account ID %s: %w", serviceAccountIDStr, err) } if serviceAccountName, ok := saIDs[serviceAccountID]; ok { return "serviceaccounts:uid:" + serviceAccountName, nil } return "", fmt.Errorf("service account ID %s not found", serviceAccountIDStr) }, nil } func (s *Service) fetchTeams(ctx context.Context, ns types.NamespaceInfo) (map[int64]string, error) { key := teamIDsCacheKey(ns.Value) res, err, _ := s.sf.Do(key, func() (any, error) { teams, err := s.identityStore.ListTeams(ctx, ns, legacy.ListTeamQuery{}) if err != nil { return nil, fmt.Errorf("could not fetch teams: %w", err) } teamIDs := make(map[int64]string, len(teams.Teams)) for _, team := range teams.Teams { teamIDs[team.ID] = team.UID } return teamIDs, nil }) if err != nil { return nil, err } teamIDs := res.(map[int64]string) s.teamIDCache.Set(ctx, key, teamIDs) return teamIDs, nil } // Should return an error if we fail to build the resolver. func (s *Service) newTeamNameResolver(ctx context.Context, ns types.NamespaceInfo) (ScopeResolverFunc, error) { teamIDs, cacheHit := s.teamIDCache.Get(ctx, teamIDsCacheKey(ns.Value)) if !cacheHit { var err error teamIDs, err = s.fetchTeams(ctx, ns) if err != nil { return nil, fmt.Errorf("could not build resolver: %w", err) } } return func(scope string) (string, error) { teamIDStr := strings.TrimPrefix(scope, "teams:id:") if teamIDStr == "" { return "", fmt.Errorf("team ID is empty") } if teamIDStr == "*" { return "teams:uid:*", nil } teamID, err := strconv.ParseInt(teamIDStr, 10, 64) if err != nil { return "", fmt.Errorf("invalid team ID %s: %w", teamIDStr, err) } if teamName, ok := teamIDs[teamID]; ok { return "teams:uid:" + teamName, nil } // Stale cache recovery: Try to fetch the teams again. if cacheHit { // Potential future improvement: if multiple threads have the same stale cache, // they might refetch teams separately and asynchronously. We could use a more sophisticated // approach to avoid this. Like checking if the cache has been updated meanwhile. cacheHit = false teamIDs, err = s.fetchTeams(ctx, ns) if err != nil { // Other improvement: Stop the calling loop if we fail to fetch teams. return "", err } if teamName, ok := teamIDs[teamID]; ok { return "teams:uid:" + teamName, nil } } return "", fmt.Errorf("team ID %s not found", teamIDStr) }, nil } func (s *Service) fetchUsers(ctx context.Context, ns types.NamespaceInfo) (map[int64]string, error) { users, err := s.identityStore.ListUsers(ctx, ns, legacy.ListUserQuery{}) if err != nil { return nil, fmt.Errorf("could not fetch users: %w", err) } userIDs := make(map[int64]string, len(users.Items)) for _, user := range users.Items { userIDs[user.ID] = user.UID } return userIDs, nil } // Should return an error if we fail to build the resolver. func (s *Service) newUserNameResolver(ctx context.Context, ns types.NamespaceInfo) (ScopeResolverFunc, error) { return func(scope string) (string, error) { userIDs, err := s.fetchUsers(ctx, ns) if err != nil { return "", fmt.Errorf("could not build resolver: %w", err) } userIDStr := strings.TrimPrefix(scope, "users:id:") if userIDStr == "" { return "", fmt.Errorf("user ID is empty") } if userIDStr == "*" { return "users:uid:*", nil } userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { return "", fmt.Errorf("invalid user ID %s: %w", userIDStr, err) } if userName, ok := userIDs[userID]; ok { return "users:uid:" + userName, nil } return "", fmt.Errorf("user ID %s not found", userIDStr) }, nil } func permissionsDelegateResolverFunc(scope string) (string, error) { if strings.TrimPrefix(scope, "permissions:type:") == "delegate" { // The permissions:type:delegate scope does not have any discriminating value, // so we return a wildcard to indicate that it applies to all roles. return "*", nil } return "", fmt.Errorf("unsupported scope: %s", scope) } func (s *Service) nameResolver(ctx context.Context, ns types.NamespaceInfo, scopePrefix string) (ScopeResolverFunc, error) { if scopePrefix == "teams:id:" { return s.newTeamNameResolver(ctx, ns) } if scopePrefix == "permissions:type:" { return permissionsDelegateResolverFunc, nil } if scopePrefix == "serviceaccounts:id:" { return s.newServiceAccountNameResolver(ctx, ns) } if scopePrefix == "users:id:" { return s.newUserNameResolver(ctx, ns) } // No resolver found for the given scope prefix. return nil, nil } // resolveScopeMap translates scopes like "teams:id:1" to "teams:uid:t1". // It assumes only one scope resolver is needed for a given scope map, based on the first valid scope encountered. func (s *Service) resolveScopeMap(ctx context.Context, ns types.NamespaceInfo, scopeMap map[string]bool) (map[string]bool, error) { var ( prefix string scopeResolver ScopeResolverFunc err error ) for scope := range scopeMap { // Find the resolver based on the first scope with a valid prefix if prefix == "" { if len(strings.Split(scope, ":")) < 3 { // Skip scopes that don't have at least 3 parts (e.g., "*", "teams:*") // This is because we expect scopes to be in the format "resource:attribute:value". continue } // Initialize the scope resolver only once prefix = accesscontrol.ScopePrefix(scope) scopeResolver, err = s.nameResolver(ctx, ns, prefix) if err != nil { s.logger.FromContext(ctx).Error("failed to create scope resolver", "prefix", prefix, "error", err) return nil, err } if scopeResolver == nil { break // No resolver found for this prefix } } // Skip scopes that do not have the expected prefix if !strings.HasPrefix(scope, prefix) { continue } resolved, err := scopeResolver(scope) if err != nil { s.logger.FromContext(ctx).Warn("could not resolve scope name", "scope", scope, "error", err) continue // Still want to process other scopes even if one fails. } if resolved != "" { scopeMap[resolved] = true delete(scopeMap, scope) } } return scopeMap, nil }