Secrets: Add namespace matches checks to authorizer and secure value client (#109651)

* Decrypt: Add namespace matches to authorizer

* SecureValueClient: Add namespace matches when auth checking
This commit is contained in:
Matheus Macabu
2025-08-14 11:50:56 +02:00
committed by GitHub
parent a38a5e0301
commit dfae5e5b4d
7 changed files with 145 additions and 103 deletions
@@ -10,43 +10,45 @@ import (
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
)
func TestDecryptAuthorizer(t *testing.T) {
tracer := noop.NewTracerProvider().Tracer("test")
defaultNs := xkube.Namespace("default")
t.Run("when no auth info is present, it returns false", func(t *testing.T) {
ctx := context.Background()
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", nil)
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.Empty(t, identity)
require.False(t, allowed)
})
t.Run("when token permissions are empty, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", nil)
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
})
t.Run("when service identity is empty, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "", []string{})
ctx := createAuthContext(context.Background(), defaultNs.String(), "", []string{})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", nil)
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.Empty(t, identity)
require.False(t, allowed)
})
t.Run("when service identity is empty string, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), " ", []string{})
ctx := createAuthContext(context.Background(), defaultNs.String(), " ", []string{})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", nil)
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.Empty(t, identity)
require.False(t, allowed)
})
@@ -55,14 +57,14 @@ func TestDecryptAuthorizer(t *testing.T) {
authorizer := ProvideDecryptAuthorizer(tracer)
// nameless
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues"})
identity, allowed := authorizer.Authorize(ctx, "", nil)
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
// named
ctx = createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/name"})
identity, allowed = authorizer.Authorize(ctx, "", nil)
ctx = createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues/name"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
})
@@ -71,32 +73,32 @@ func TestDecryptAuthorizer(t *testing.T) {
authorizer := ProvideDecryptAuthorizer(tracer)
// nameless
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:*"})
identity, allowed := authorizer.Authorize(ctx, "", nil)
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues:*"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
// named
ctx = createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/name:something"})
identity, allowed = authorizer.Authorize(ctx, "", nil)
ctx = createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues/name:something"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
})
t.Run("when permission does not have 2 or 3 parts, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app:decrypt"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", nil)
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
})
t.Run("when permission has group that is not `secret.grafana.app`, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"wrong.group/securevalues/invalid:decrypt"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"wrong.group/securevalues/invalid:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", nil)
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
})
@@ -105,23 +107,23 @@ func TestDecryptAuthorizer(t *testing.T) {
authorizer := ProvideDecryptAuthorizer(tracer)
// nameless
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/invalid-resource:decrypt"})
identity, allowed := authorizer.Authorize(ctx, "", nil)
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/invalid-resource:decrypt"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
// named
ctx = createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/invalid-resource/name:decrypt"})
identity, allowed = authorizer.Authorize(ctx, "", nil)
ctx = createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/invalid-resource/name:decrypt"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "", nil)
require.NotEmpty(t, identity)
require.False(t, allowed)
})
t.Run("when the allow list is empty, it allows all identities", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", []string{"identity"})
require.NotEmpty(t, identity)
require.True(t, allowed)
})
@@ -130,14 +132,14 @@ func TestDecryptAuthorizer(t *testing.T) {
authorizer := ProvideDecryptAuthorizer(tracer)
// nameless
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
identity, allowed := authorizer.Authorize(ctx, "", []string{"group2"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", []string{"group2"})
require.NotEmpty(t, identity)
require.False(t, allowed)
// named
ctx = createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/name:decrypt"})
identity, allowed = authorizer.Authorize(ctx, "", []string{"group2"})
ctx = createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues/name:decrypt"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "", []string{"group2"})
require.NotEmpty(t, identity)
require.False(t, allowed)
})
@@ -146,20 +148,20 @@ func TestDecryptAuthorizer(t *testing.T) {
authorizer := ProvideDecryptAuthorizer(tracer)
// nameless
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", []string{"identity"})
require.True(t, allowed)
require.Equal(t, "identity", identity)
// named
ctx = createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/name:decrypt"})
identity, allowed = authorizer.Authorize(ctx, "name", []string{"identity"})
ctx = createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues/name:decrypt"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "name", []string{"identity"})
require.True(t, allowed)
require.Equal(t, "identity", identity)
})
t.Run("when there are multiple permissions, some invalid, only valid ones are considered", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{
"secret.grafana.app/securevalues/name1:decrypt",
"secret.grafana.app/securevalues/name2:decrypt",
"secret.grafana.app/securevalues/invalid:read",
@@ -168,47 +170,47 @@ func TestDecryptAuthorizer(t *testing.T) {
})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "name1", []string{"identity"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "name1", []string{"identity"})
require.True(t, allowed)
require.Equal(t, "identity", identity)
identity, allowed = authorizer.Authorize(ctx, "name2", []string{"identity"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "name2", []string{"identity"})
require.True(t, allowed)
require.Equal(t, "identity", identity)
})
t.Run("when empty secure value name with specific permission, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/name:decrypt"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues/name:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", []string{"identity"})
require.Equal(t, "identity", identity)
require.False(t, allowed)
})
t.Run("when permission has an extra / but no name, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/:decrypt"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues/:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", []string{"identity"})
require.Equal(t, "identity", identity)
require.False(t, allowed)
})
t.Run("when the decrypters list is empty, meaning nothing can decrypt the secure value, it returns false", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "name", []string{})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "name", []string{})
require.Equal(t, "identity", identity)
require.False(t, allowed)
})
t.Run("when one of decrypters matches the identity, it returns true", func(t *testing.T) {
ctx := createAuthContext(context.Background(), "identity1", []string{"secret.grafana.app/securevalues:decrypt"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity1", []string{"secret.grafana.app/securevalues:decrypt"})
authorizer := ProvideDecryptAuthorizer(tracer)
identity, allowed := authorizer.Authorize(ctx, "", []string{"identity1", "identity2", "identity3"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", []string{"identity1", "identity2", "identity3"})
require.Equal(t, "identity1", identity)
require.True(t, allowed)
})
@@ -216,25 +218,35 @@ func TestDecryptAuthorizer(t *testing.T) {
t.Run("permissions must be case-sensitive and return false", func(t *testing.T) {
authorizer := ProvideDecryptAuthorizer(tracer)
ctx := createAuthContext(context.Background(), "identity", []string{"SECRET.grafana.app/securevalues:decrypt"})
identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"})
ctx := createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"SECRET.grafana.app/securevalues:decrypt"})
identity, allowed := authorizer.Authorize(ctx, defaultNs, "", []string{"identity"})
require.Equal(t, "identity", identity)
require.False(t, allowed)
ctx = createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/SECUREVALUES:decrypt"})
identity, allowed = authorizer.Authorize(ctx, "", []string{"identity"})
ctx = createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/SECUREVALUES:decrypt"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "", []string{"identity"})
require.Equal(t, "identity", identity)
require.False(t, allowed)
ctx = createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:DECRYPT"})
identity, allowed = authorizer.Authorize(ctx, "", []string{"identity"})
ctx = createAuthContext(context.Background(), defaultNs.String(), "identity", []string{"secret.grafana.app/securevalues:DECRYPT"})
identity, allowed = authorizer.Authorize(ctx, defaultNs, "", []string{"identity"})
require.Equal(t, "identity", identity)
require.False(t, allowed)
})
t.Run("when namespace doesn't match the token's, it returns false", func(t *testing.T) {
authorizer := ProvideDecryptAuthorizer(tracer)
ctx := createAuthContext(context.Background(), "namespace1", "identity", []string{"secret.grafana.app/securevalues:decrypt"})
identity, allowed := authorizer.Authorize(ctx, "namespace2", "", []string{"identity"})
require.Empty(t, identity)
require.False(t, allowed)
})
}
func createAuthContext(ctx context.Context, serviceIdentity string, permissions []string) context.Context {
func createAuthContext(ctx context.Context, namespace string, serviceIdentity string, permissions []string) context.Context {
requester := &identity.StaticRequester{
Namespace: namespace,
AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
Rest: authn.AccessTokenClaims{
Permissions: permissions,