Files
grafana/pkg/services/authz/rbac/resolver.go
Gabriel MABILLE 69dc5a0b88 grafana-iam: Add resolver for permissions:type:delegate (#108789)
* `grafana-iam`: Add resolver for `permissions:type:delegate`

* roles create -> write
2025-07-29 21:11:06 +02:00

149 lines
4.6 KiB
Go

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) 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 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
}
// 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
}