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
pull/107772/head
Matheus Macabu 2 weeks ago committed by GitHub
parent e4650d3d8f
commit b6c4788c2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 67
      pkg/apimachinery/identity/context.go
  2. 46
      pkg/apimachinery/identity/context_test.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{

@ -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…
Cancel
Save