From 16fda6f68609ff3e472fcfbb08187fb38f68b2a2 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:54:47 +0100 Subject: [PATCH] Authz: Setup access claims for service identity (#100986) * Setup access claims for service identity and add them to identityes without any claims --- pkg/apimachinery/identity/context.go | 67 ++++++++++++------- pkg/apimachinery/identity/static.go | 15 +++-- pkg/services/authn/authnimpl/registration.go | 2 + .../authn/authnimpl/sync/access_claims.go | 32 +++++++++ .../authn/grpcutils/inproc_exchanger.go | 5 +- 5 files changed, 89 insertions(+), 32 deletions(-) create mode 100644 pkg/services/authn/authnimpl/sync/access_claims.go diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 7600f000e7e..8699b3a3ad9 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" ) @@ -30,27 +31,34 @@ func checkNilRequester(r Requester) bool { return r == nil || (reflect.ValueOf(r).Kind() == reflect.Ptr && reflect.ValueOf(r).IsNil()) } -const serviceName = "service" -const serviceNameForProvisioning = "provisioning" +const ( + serviceName = "service" + serviceNameForProvisioning = "provisioning" +) -// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. -// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with -// static permissions so it can be used in legacy code paths. -func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) { - r := &StaticRequester{ +func newInternalIdentity(name string, namespace string, orgID int64) Requester { + return &StaticRequester{ Type: types.TypeAccessPolicy, - Name: serviceName, - UserUID: serviceName, - AuthID: serviceName, - Login: serviceName, + Name: name, + UserUID: name, + AuthID: name, + Login: name, OrgRole: RoleAdmin, + Namespace: namespace, IsGrafanaAdmin: true, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: serviceIdentityPermissions, }, + AccessTokenClaims: ServiceIdentityClaims, } +} +// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. +// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with +// static permissions so it can be used in legacy code paths. +func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) { + r := newInternalIdentity(serviceName, "", orgID) return WithRequester(ctx, r), r } @@ -60,21 +68,7 @@ func WithProvisioningIdentitiy(ctx context.Context, namespace string) (context.C return nil, nil, err } - r := &StaticRequester{ - Type: types.TypeAccessPolicy, - Name: serviceNameForProvisioning, - UserUID: serviceNameForProvisioning, - AuthID: serviceNameForProvisioning, - Login: serviceNameForProvisioning, - OrgRole: RoleAdmin, - IsGrafanaAdmin: true, - Namespace: namespace, - OrgID: ns.OrgID, - Permissions: map[int64]map[string][]string{ - ns.OrgID: serviceIdentityPermissions, - }, - } - + r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID) return WithRequester(ctx, r), r, nil } @@ -97,6 +91,14 @@ func getWildcardPermissions(actions ...string) map[string][]string { return permissions } +func getTokenPermissions(groups ...string) []string { + out := make([]string, 0, len(groups)) + for _, group := range groups { + out = append(out, group+":*") + } + return out +} + // serviceIdentityPermissions is a list of wildcard permissions for provided actions. // We should add every action required "internally" here. var serviceIdentityPermissions = getWildcardPermissions( @@ -118,6 +120,19 @@ var serviceIdentityPermissions = getWildcardPermissions( "teams:read", // accesscontrol.ActionTeamsRead, ) +var serviceIdentityTokenPermissions = getTokenPermissions( + "folder.grafana.app", + "dashboard.grafana.app", + "secret.grafana.app", +) + +var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: serviceIdentityTokenPermissions, + DelegatedPermissions: serviceIdentityTokenPermissions, + }, +} + func IsServiceIdentity(ctx context.Context) bool { ident, ok := types.AuthInfoFrom(ctx) if !ok { diff --git a/pkg/apimachinery/identity/static.go b/pkg/apimachinery/identity/static.go index 05678db4568..e948f2c1e65 100644 --- a/pkg/apimachinery/identity/static.go +++ b/pkg/apimachinery/identity/static.go @@ -30,10 +30,11 @@ type StaticRequester struct { Namespace string IsGrafanaAdmin bool // Permissions grouped by orgID and actions - Permissions map[int64]map[string][]string - IDToken string - IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] - CacheKey string + Permissions map[int64]map[string][]string + IDToken string + IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] + AccessTokenClaims *authnlib.Claims[authnlib.AccessTokenClaims] + CacheKey string } // GetID returns typed id for the entity @@ -62,10 +63,16 @@ func (u *StaticRequester) GetAudience() []string { } func (u *StaticRequester) GetTokenPermissions() []string { + if u.AccessTokenClaims != nil { + return u.AccessTokenClaims.Rest.Permissions + } return []string{} } func (u *StaticRequester) GetTokenDelegatedPermissions() []string { + if u.AccessTokenClaims != nil { + return u.AccessTokenClaims.Rest.DelegatedPermissions + } return []string{} } diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index 593a0d823fc..c5af37a3098 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -145,5 +145,7 @@ func ProvideRegistration( nsSync := sync.ProvideNamespaceSync(cfg) authnSvc.RegisterPostAuthHook(nsSync.SyncNamespace, 150) + authnSvc.RegisterPostAuthHook(sync.AccessClaimsHook, 160) + return Registration{} } diff --git a/pkg/services/authn/authnimpl/sync/access_claims.go b/pkg/services/authn/authnimpl/sync/access_claims.go new file mode 100644 index 00000000000..e66bc440721 --- /dev/null +++ b/pkg/services/authn/authnimpl/sync/access_claims.go @@ -0,0 +1,32 @@ +package sync + +import ( + "context" + + authnlib "github.com/grafana/authlib/authn" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/authn" +) + +func NewAccessClaimsSync() AccessClaimsSync { + return AccessClaimsSync{} +} + +type AccessClaimsSync struct{} + +func AccessClaimsHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error { + if id.AccessTokenClaims == nil { + // When normal authencation flows are used withint grafana we don't have any access token e.g. using user + // session. This makes it impossible to authorize using AccessClient because we don't have any access claims + // with deletegated permissions. To get around this we use the hardcoded delegated + // permissions. + id.AccessTokenClaims = &authnlib.Claims[authnlib.AccessTokenClaims]{ + Rest: authnlib.AccessTokenClaims{ + DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions, + }, + } + } + + return nil +} diff --git a/pkg/services/authn/grpcutils/inproc_exchanger.go b/pkg/services/authn/grpcutils/inproc_exchanger.go index f7e24ffed7b..af252ca2fb1 100644 --- a/pkg/services/authn/grpcutils/inproc_exchanger.go +++ b/pkg/services/authn/grpcutils/inproc_exchanger.go @@ -9,6 +9,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" ) type inProcExchanger struct { @@ -37,8 +38,8 @@ func createInProcToken() (*authn.TokenExchangeResponse, error) { }, Rest: authn.AccessTokenClaims{ Namespace: "*", - Permissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"}, - DelegatedPermissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"}, + Permissions: identity.ServiceIdentityClaims.Rest.Permissions, + DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions, }, }