149 lines
4.6 KiB
Go
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
|
|
}
|