mirror of https://github.com/grafana/grafana.git
				
				
				
			Auth: Add functional option for static requester methods (#107581)
* Auth: Add functional option for static requester methods Initially supporting WithServiceIdentityName to set a ServiceIdentity inside the Claims.Rest object, so that Secrets Manager can parse the service requesting secret decryption. On Secret creation, the service will have to pass its identity (which is a freeform string) to the SecureValues' Decrypters object. This field gates which services are allowed to decrypt the SecureValue. And upon decryption, the service should build a static identity with that same service identity name when calling the decrypt service. * StaticRequester: Put secret decrypt permission in access token claims * StaticRequester: Inline getTokenPermissions function
This commit is contained in:
		
							parent
							
								
									e4650d3d8f
								
							
						
					
					
						commit
						b6c4788c2a
					
				|  | @ -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{ | ||||
|  |  | |||
|  | @ -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)]) | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue