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"
|
serviceNameForProvisioning = "provisioning"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newInternalIdentity(name string, namespace string, orgID int64) Requester {
|
type IdentityOpts func(*StaticRequester)
|
||||||
return &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,
|
Type: types.TypeAccessPolicy,
|
||||||
Name: name,
|
Name: name,
|
||||||
UserUID: name,
|
UserUID: name,
|
||||||
|
|
@ -50,37 +64,43 @@ func newInternalIdentity(name string, namespace string, orgID int64) Requester {
|
||||||
Permissions: map[int64]map[string][]string{
|
Permissions: map[int64]map[string][]string{
|
||||||
orgID: serviceIdentityPermissions,
|
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.
|
// 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
|
// 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.
|
// static permissions so it can be used in legacy code paths.
|
||||||
func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) {
|
func WithServiceIdentity(ctx context.Context, orgID int64, opts ...IdentityOpts) (context.Context, Requester) {
|
||||||
r := newInternalIdentity(serviceName, "*", orgID)
|
r := newInternalIdentity(serviceName, "*", orgID, opts...)
|
||||||
return WithRequester(ctx, r), r
|
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)
|
ns, err := types.ParseNamespace(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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
|
return WithRequester(ctx, r), r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithServiceIdentityContext sets an identity representing the service itself in context.
|
// WithServiceIdentityContext sets an identity representing the service itself in context.
|
||||||
func WithServiceIdentityContext(ctx context.Context, orgID int64) context.Context {
|
func WithServiceIdentityContext(ctx context.Context, orgID int64, opts ...IdentityOpts) context.Context {
|
||||||
ctx, _ = WithServiceIdentity(ctx, orgID)
|
ctx, _ = WithServiceIdentity(ctx, orgID, opts...)
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithServiceIdentityFN calls provided closure with an context contaning the identity of the service.
|
// 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) {
|
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))
|
return fn(WithServiceIdentityContext(ctx, orgID, opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWildcardPermissions(actions ...string) map[string][]string {
|
func getWildcardPermissions(actions ...string) map[string][]string {
|
||||||
|
|
@ -91,14 +111,6 @@ func getWildcardPermissions(actions ...string) map[string][]string {
|
||||||
return permissions
|
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.
|
// serviceIdentityPermissions is a list of wildcard permissions for provided actions.
|
||||||
// We should add every action required "internally" here.
|
// We should add every action required "internally" here.
|
||||||
var serviceIdentityPermissions = getWildcardPermissions(
|
var serviceIdentityPermissions = getWildcardPermissions(
|
||||||
|
|
@ -121,13 +133,16 @@ var serviceIdentityPermissions = getWildcardPermissions(
|
||||||
"serviceaccounts:read", // serviceaccounts.ActionRead,
|
"serviceaccounts:read", // serviceaccounts.ActionRead,
|
||||||
)
|
)
|
||||||
|
|
||||||
var serviceIdentityTokenPermissions = getTokenPermissions(
|
var serviceIdentityTokenPermissions = []string{
|
||||||
"folder.grafana.app",
|
"folder.grafana.app:*",
|
||||||
"dashboard.grafana.app",
|
"dashboard.grafana.app:*",
|
||||||
"secret.grafana.app",
|
"secret.grafana.app:*",
|
||||||
"query.grafana.app",
|
"query.grafana.app:*",
|
||||||
"iam.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]{
|
var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{
|
||||||
Rest: authn.AccessTokenClaims{
|
Rest: authn.AccessTokenClaims{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/authn"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
|
@ -24,3 +25,48 @@ func TestRequesterFromContext(t *testing.T) {
|
||||||
require.Equal(t, expected.GetUID(), actual.GetUID())
|
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