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