diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 4e849dbba3c..f98738db58d 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -36,8 +36,22 @@ const ( serviceNameForProvisioning = "provisioning" ) -func newInternalIdentity(name string, namespace string, orgID int64) Requester { - return &StaticRequester{ +type IdentityOpts func(*StaticRequester) + +// WithServiceIdentityName sets the `StaticRequester.AccessTokenClaims.Rest.ServiceIdentity` field to the provided name. +// This is so far only used by Secrets Manager to identify and gate the service decrypting a secret. +func WithServiceIdentityName(name string) IdentityOpts { + return func(r *StaticRequester) { + r.AccessTokenClaims.Rest.ServiceIdentity = name + } +} + +func newInternalIdentity(name string, namespace string, orgID int64, opts ...IdentityOpts) Requester { + // Create a copy of the ServiceIdentityClaims to avoid modifying the global one. + // Some of the options might mutate it. + claimsCopy := *ServiceIdentityClaims + + staticRequester := &StaticRequester{ Type: types.TypeAccessPolicy, Name: name, UserUID: name, @@ -50,37 +64,43 @@ func newInternalIdentity(name string, namespace string, orgID int64) Requester { Permissions: map[int64]map[string][]string{ orgID: serviceIdentityPermissions, }, - AccessTokenClaims: ServiceIdentityClaims, + AccessTokenClaims: &claimsCopy, } + + for _, opt := range opts { + opt(staticRequester) + } + + return staticRequester } // 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) +func WithServiceIdentity(ctx context.Context, orgID int64, opts ...IdentityOpts) (context.Context, Requester) { + r := newInternalIdentity(serviceName, "*", orgID, opts...) return WithRequester(ctx, r), r } -func WithProvisioningIdentity(ctx context.Context, namespace string) (context.Context, Requester, error) { +func WithProvisioningIdentity(ctx context.Context, namespace string, opts ...IdentityOpts) (context.Context, Requester, error) { ns, err := types.ParseNamespace(namespace) if err != nil { return nil, nil, err } - r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID) + r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID, opts...) return WithRequester(ctx, r), r, nil } // WithServiceIdentityContext sets an identity representing the service itself in context. -func WithServiceIdentityContext(ctx context.Context, orgID int64) context.Context { - ctx, _ = WithServiceIdentity(ctx, orgID) +func WithServiceIdentityContext(ctx context.Context, orgID int64, opts ...IdentityOpts) context.Context { + ctx, _ = WithServiceIdentity(ctx, orgID, opts...) return ctx } // WithServiceIdentityFN calls provided closure with an context contaning the identity of the service. -func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error)) (T, error) { - return fn(WithServiceIdentityContext(ctx, orgID)) +func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error), opts ...IdentityOpts) (T, error) { + return fn(WithServiceIdentityContext(ctx, orgID, opts...)) } func getWildcardPermissions(actions ...string) map[string][]string { @@ -91,14 +111,6 @@ 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( @@ -121,13 +133,16 @@ var serviceIdentityPermissions = getWildcardPermissions( "serviceaccounts:read", // serviceaccounts.ActionRead, ) -var serviceIdentityTokenPermissions = getTokenPermissions( - "folder.grafana.app", - "dashboard.grafana.app", - "secret.grafana.app", - "query.grafana.app", - "iam.grafana.app", -) +var serviceIdentityTokenPermissions = []string{ + "folder.grafana.app:*", + "dashboard.grafana.app:*", + "secret.grafana.app:*", + "query.grafana.app:*", + "iam.grafana.app:*", + + // Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions. + "secret.grafana.app/securevalues:decrypt", +} var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{ Rest: authn.AccessTokenClaims{ diff --git a/pkg/apimachinery/identity/context_test.go b/pkg/apimachinery/identity/context_test.go index 87f31a17763..10c043517d7 100644 --- a/pkg/apimachinery/identity/context_test.go +++ b/pkg/apimachinery/identity/context_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/grafana/authlib/authn" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -24,3 +25,48 @@ func TestRequesterFromContext(t *testing.T) { require.Equal(t, expected.GetUID(), actual.GetUID()) }) } + +func TestWithServiceIdentity(t *testing.T) { + t.Run("with a custom service identity name", func(t *testing.T) { + customName := "custom-service" + orgID := int64(1) + ctx, requester := identity.WithServiceIdentity(context.Background(), orgID, identity.WithServiceIdentityName(customName)) + require.NotNil(t, requester) + require.Equal(t, orgID, requester.GetOrgID()) + require.Equal(t, customName, requester.GetExtra()[string(authn.ServiceIdentityKey)][0]) + require.Contains(t, requester.GetTokenPermissions(), "secret.grafana.app/securevalues:decrypt") + + fromCtx, err := identity.GetRequester(ctx) + require.NoError(t, err) + require.Equal(t, customName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0]) + + // Reuse the context but create another identity on top with a different name and org ID + anotherCustomName := "another-custom-service" + anotherOrgID := int64(2) + ctx2 := identity.WithServiceIdentityContext(ctx, anotherOrgID, identity.WithServiceIdentityName(anotherCustomName)) + + fromCtx, err = identity.GetRequester(ctx2) + require.NoError(t, err) + require.Equal(t, anotherOrgID, fromCtx.GetOrgID()) + require.Equal(t, anotherCustomName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0]) + + // Reuse the context but create another identity without a custom name + ctx3, requester := identity.WithServiceIdentity(ctx2, 1) + require.NotNil(t, requester) + require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)]) + + fromCtx, err = identity.GetRequester(ctx3) + require.NoError(t, err) + require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)]) + }) + + t.Run("without a custom service identity name", func(t *testing.T) { + ctx, requester := identity.WithServiceIdentity(context.Background(), 1) + require.NotNil(t, requester) + require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)]) + + fromCtx, err := identity.GetRequester(ctx) + require.NoError(t, err) + require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)]) + }) +}