Secrets: add authorizer for CRUDL kind operations (#100498)

* Secrets: add authorizer for CRUDL kind operations

* more unit tests

* fix integration tests and add helper to create users with arbitrary permissions

* split actions by kind

* fix integration tests resource permissions passing

* Add integration tests for lack permissions keepers

* Add integration tests for lack permissions securevalues

* Fine tune permissions for integration tests

* Clarify access roles naming

* Linter: preallocate permissions in test helper

* Add integration tests for scoped permissions on resources

* Merge list+describe actions into read for both resources

* Update authorizer to use generic MT one with action mapper

* Rename secrets-manager in permissions to just secret

* Keeper: fix Update sometimes failing because of missing condition clause on name+namespace

* Also grant reading permission to writers as best practice

* Add note on resource authorizer not being folder dependent
pull/100997/head
Matheus Macabu 3 months ago committed by GitHub
parent 0a5d026602
commit d6ca7a2c86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 124
      pkg/registry/apis/secret/accesscontrol.go
  2. 33
      pkg/registry/apis/secret/register.go
  3. 3
      pkg/services/accesscontrol/permreg/permreg.go
  4. 4
      pkg/services/authz/rbac/mapper.go
  5. 4
      pkg/storage/secret/metadata/keeper_store.go
  6. 354
      pkg/tests/apis/secret/keeper_test.go
  7. 68
      pkg/tests/apis/secret/main_test.go
  8. 347
      pkg/tests/apis/secret/secure_value_test.go

@ -0,0 +1,124 @@
package secret
import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
const (
// SecureValues
ActionSecretSecureValuesCreate = "secret.securevalues:create" // CREATE.
ActionSecretSecureValuesWrite = "secret.securevalues:write" // UPDATE.
ActionSecretSecureValuesRead = "secret.securevalues:read" // GET + LIST.
ActionSecretSecureValuesDelete = "secret.securevalues:delete" // DELETE.
// Keepers
ActionSecretKeepersCreate = "secret.keepers:create" // CREATE.
ActionSecretKeepersWrite = "secret.keepers:write" // UPDATE.
ActionSecretKeepersRead = "secret.keepers:read" // GET + LIST.
ActionSecretKeepersDelete = "secret.keepers:delete" // DELETE.
)
var (
ScopeProviderSecretSecureValues = accesscontrol.NewScopeProvider("secret.securevalues")
ScopeProviderSecretKeepers = accesscontrol.NewScopeProvider("secret.keepers")
ScopeAllSecureValues = ScopeProviderSecretSecureValues.GetResourceAllScope()
ScopeAllKeepers = ScopeProviderSecretKeepers.GetResourceAllScope()
)
func RegisterAccessControlRoles(service accesscontrol.Service) error {
// SecureValues
secureValuesReader := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:secret.securevalues:reader",
DisplayName: "Secrets Manager secure values reader",
Description: "Read and list secure values.",
Group: "Secrets Manager",
Permissions: []accesscontrol.Permission{
{
Action: ActionSecretSecureValuesRead,
Scope: ScopeAllSecureValues,
},
},
},
Grants: []string{string(accesscontrol.RoleGrafanaAdmin)},
}
secureValuesWriter := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:secret.securevalues:writer",
DisplayName: "Secrets Manager secure values writer",
Description: "Create, update and delete secure values.",
Group: "Secrets Manager",
Permissions: []accesscontrol.Permission{
{
Action: ActionSecretSecureValuesCreate,
Scope: ScopeAllSecureValues,
},
{
Action: ActionSecretSecureValuesRead,
Scope: ScopeAllSecureValues,
},
{
Action: ActionSecretSecureValuesWrite,
Scope: ScopeAllSecureValues,
},
{
Action: ActionSecretSecureValuesDelete,
Scope: ScopeAllSecureValues,
},
},
},
Grants: []string{string(accesscontrol.RoleGrafanaAdmin)},
}
// Keepers
keepersReader := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:secret.keepers:reader",
DisplayName: "Secrets Manager keepers reader",
Description: "Read and list keepers.",
Group: "Secrets Manager",
Permissions: []accesscontrol.Permission{
{
Action: ActionSecretKeepersRead,
Scope: ScopeAllKeepers,
},
},
},
Grants: []string{string(accesscontrol.RoleGrafanaAdmin)},
}
keepersWriter := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:secret.keepers:writer",
DisplayName: "Secrets Manager keepers writer",
Description: "Create, update and delete keepers.",
Group: "Secrets Manager",
Permissions: []accesscontrol.Permission{
{
Action: ActionSecretKeepersCreate,
Scope: ScopeAllKeepers,
},
{
Action: ActionSecretKeepersRead,
Scope: ScopeAllKeepers,
},
{
Action: ActionSecretKeepersWrite,
Scope: ScopeAllKeepers,
},
{
Action: ActionSecretKeepersDelete,
Scope: ScopeAllKeepers,
},
},
},
Grants: []string{string(accesscontrol.RoleGrafanaAdmin)},
}
return service.DeclareFixedRoles(
secureValuesReader, secureValuesWriter,
keepersReader, keepersWriter,
)
}

@ -15,10 +15,13 @@ import (
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
claims "github.com/grafana/authlib/types"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/reststorage"
"github.com/grafana/grafana/pkg/services/accesscontrol"
authsvc "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
@ -38,10 +41,17 @@ type SecretAPIBuilder struct {
secureValueStorage contracts.SecureValueStorage
keeperStorage contracts.KeeperStorage
decryptStorage contracts.DecryptStorage
accessClient claims.AccessClient
}
func NewSecretAPIBuilder(tracer tracing.Tracer, secureValueStorage contracts.SecureValueStorage, keeperStorage contracts.KeeperStorage, decryptStorage contracts.DecryptStorage) *SecretAPIBuilder {
return &SecretAPIBuilder{tracer, secureValueStorage, keeperStorage, decryptStorage}
func NewSecretAPIBuilder(
tracer tracing.Tracer,
secureValueStorage contracts.SecureValueStorage,
keeperStorage contracts.KeeperStorage,
decryptStorage contracts.DecryptStorage,
accessClient claims.AccessClient,
) *SecretAPIBuilder {
return &SecretAPIBuilder{tracer, secureValueStorage, keeperStorage, decryptStorage, accessClient}
}
func RegisterAPIService(
@ -52,6 +62,8 @@ func RegisterAPIService(
secureValueStorage contracts.SecureValueStorage,
keeperStorage contracts.KeeperStorage,
decryptStorage contracts.DecryptStorage,
accessClient claims.AccessClient,
accessControlService accesscontrol.Service,
) (*SecretAPIBuilder, error) {
// Skip registration unless opting into experimental apis and the secrets management app platform flag.
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
@ -68,7 +80,11 @@ func RegisterAPIService(
decryptStorage = reststorage.NewFakeDecryptStore(secureValueStorage)
}
builder := NewSecretAPIBuilder(tracer, secureValueStorage, keeperStorage, decryptStorage)
if err := RegisterAccessControlRoles(accessControlService); err != nil {
return nil, fmt.Errorf("register secret access control roles: %w", err)
}
builder := NewSecretAPIBuilder(tracer, secureValueStorage, keeperStorage, decryptStorage, accessClient)
apiregistration.RegisterAPI(builder)
return builder, nil
}
@ -131,11 +147,14 @@ func (b *SecretAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions
return secretv0alpha1.GetOpenAPIDefinitions
}
// GetAuthorizer: [TODO] who can create secrets? must be multi-tenant first
// GetAuthorizer decides whether the request is allowed, denied or no opinion based on credentials and request attributes.
// Usually most resource are stored in folders (e.g. alerts, dashboards), which allows users to manage permissions at folder level,
// rather than at resource level which also has the benefit of lowering the load on AuthZ side, since instead of storing access to
// a single dashboard, you'd store access to all dashboards in a specific folder.
// For Secrets, this is not the case, but if we want to make it so, we need to update this ResourceAuthorizer to check the containing folder.
// If we ever want to do that, get guidance from IAM first as well.
func (b *SecretAPIBuilder) GetAuthorizer() authorizer.Authorizer {
// This is TBD being defined with IAM. Test
return nil // start with the default authorizer
return authsvc.NewResourceAuthorizer(b.accessClient)
}
// Register additional routes with the server.

@ -94,6 +94,9 @@ func newPermissionRegistry() *permissionRegistry {
"roles": "roles:uid:",
"services": "services:",
"receivers": "receivers:uid:",
"secret.securevalues": "secret.securevalues:uid:",
"secret.keepers": "secret.keepers:uid:",
}
return &permissionRegistry{
actionScopePrefixes: make(map[string]PrefixSet, 200),

@ -63,6 +63,10 @@ func newMapper() mapper {
"iam.grafana.app": {
"teams": newResourceTranslation("teams", "id", false),
},
"secret.grafana.app": {
"securevalues": newResourceTranslation("secret.securevalues", "uid", false),
"keepers": newResourceTranslation("secret.keepers", "uid", false),
},
}
}

@ -129,7 +129,9 @@ func (s *keeperStorage) Update(ctx context.Context, newKeeper *secretv0alpha1.Ke
return nil, fmt.Errorf("failed to map into update row: %w", err)
}
err = s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
if _, err := sess.Update(newRow); err != nil {
cond := &keeperDB{Name: newKeeper.Name, Namespace: newKeeper.Namespace}
if _, err := sess.Update(newRow, cond); err != nil {
return fmt.Errorf("failed to update row: %w", err)
}

@ -3,11 +3,14 @@ package secret
import (
"context"
"errors"
"math/rand/v2"
"net/http"
"strconv"
"strings"
"testing"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
@ -29,6 +32,9 @@ func TestIntegrationKeeper(t *testing.T) {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // required for experimental APIs
EnableFeatureToggles: []string{
@ -38,12 +44,12 @@ func TestIntegrationKeeper(t *testing.T) {
},
})
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
permissions := map[string]ResourcePermission{ResourceKeepers: {Actions: ActionsAllKeepers}}
genericUserEditor := mustCreateUsers(t, helper, permissions).Editor
client := helper.GetResourceClient(apis.ResourceClientArgs{
// #TODO: figure out permissions topic
User: helper.Org1.Admin,
User: genericUserEditor,
GVR: gvrKeepers,
})
@ -63,7 +69,7 @@ func TestIntegrationKeeper(t *testing.T) {
})
t.Run("creating a keeper returns it", func(t *testing.T) {
raw := mustGenerateKeeper(t, helper, helper.Org1.Admin, nil)
raw := mustGenerateKeeper(t, helper, genericUserEditor, nil)
keeper := new(secretv0alpha1.Keeper)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, keeper)
@ -155,7 +161,7 @@ func TestIntegrationKeeper(t *testing.T) {
})
t.Run("creating a keeper with a provider then changing the provider does not return an error", func(t *testing.T) {
rawAWS := mustGenerateKeeper(t, helper, helper.Org1.Admin, nil)
rawAWS := mustGenerateKeeper(t, helper, genericUserEditor, nil)
testDataKeeperGCP := rawAWS.DeepCopy()
testDataKeeperGCP.Object["spec"].(map[string]any)["aws"] = nil
@ -224,11 +230,30 @@ func TestIntegrationKeeper(t *testing.T) {
})
t.Run("creating a keeper that references a securevalue that is stored in a non-SQL type Keeper returns an error", func(t *testing.T) {
// 0. Create user with required permissions.
permissions := map[string]ResourcePermission{
ResourceKeepers: {Actions: ActionsAllKeepers},
// needed for this test to create (and delete for cleanup) securevalues.
ResourceSecureValues: {
Actions: []string{
secret.ActionSecretSecureValuesCreate,
secret.ActionSecretSecureValuesDelete,
},
},
}
editor := mustCreateUsers(t, helper, permissions).Editor
clientP := helper.GetResourceClient(apis.ResourceClientArgs{
User: editor,
GVR: gvrKeepers,
})
// 1. Create a non-SQL keeper without using `secureValueName`.
keeperAWS := mustGenerateKeeper(t, helper, helper.Org1.Admin, nil)
keeperAWS := mustGenerateKeeper(t, helper, editor, nil)
// 2. Create a secureValue that is stored in the previously created keeper (non-SQL).
secureValue := mustGenerateSecureValue(t, helper, helper.Org1.Admin, keeperAWS.GetName())
secureValue := mustGenerateSecureValue(t, helper, editor, keeperAWS.GetName())
// 3. Create another keeper that uses the secureValue, which fails.
testDataAnotherKeeper := keeperAWS.DeepCopy()
@ -236,14 +261,28 @@ func TestIntegrationKeeper(t *testing.T) {
"secureValueName": secureValue.GetName(),
}
keeperAnother, err := client.Resource.Create(ctx, testDataAnotherKeeper, metav1.CreateOptions{})
keeperAnother, err := clientP.Resource.Create(ctx, testDataAnotherKeeper, metav1.CreateOptions{})
require.Error(t, err)
require.Nil(t, keeperAnother)
})
t.Run("creating a keeper that references a securevalue that is stored in a SQL type Keeper returns no error", func(t *testing.T) {
// 0. Create user with required permissions.
permissions := map[string]ResourcePermission{
ResourceKeepers: {Actions: ActionsAllKeepers},
// needed for this test to create (and delete for cleanup) securevalues.
ResourceSecureValues: {
Actions: []string{
secret.ActionSecretSecureValuesCreate,
secret.ActionSecretSecureValuesDelete,
},
},
}
editor := mustCreateUsers(t, helper, permissions).Editor
// 1. Create a SQL keeper.
keeperSQL := mustGenerateKeeper(t, helper, helper.Org1.Admin, map[string]any{
keeperSQL := mustGenerateKeeper(t, helper, editor, map[string]any{
"title": "SQL Keeper",
"sql": map[string]any{
"encryption": map[string]any{"envelope": map[string]any{}},
@ -251,10 +290,10 @@ func TestIntegrationKeeper(t *testing.T) {
})
// 2. Create a secureValue that is stored in the previously created keeper (SQL).
secureValue := mustGenerateSecureValue(t, helper, helper.Org1.Admin, keeperSQL.GetName())
secureValue := mustGenerateSecureValue(t, helper, editor, keeperSQL.GetName())
// 3. Create a non-SQL keeper that uses the secureValue.
keeperAWS := mustGenerateKeeper(t, helper, helper.Org1.Admin, map[string]any{
keeperAWS := mustGenerateKeeper(t, helper, editor, map[string]any{
"title": "AWS Keeper",
"aws": map[string]any{
"accessKeyId": map[string]any{"secureValueName": secureValue.GetName()},
@ -265,50 +304,54 @@ func TestIntegrationKeeper(t *testing.T) {
})
t.Run("creating keepers in multiple namespaces", func(t *testing.T) {
adminOrg1 := helper.Org1.Admin
adminOrg2 := helper.OrgB.Admin
permissions := map[string]ResourcePermission{
ResourceKeepers: {Actions: ActionsAllKeepers},
}
editorOrgA := mustCreateUsers(t, helper, permissions).Editor
editorOrgB := mustCreateUsers(t, helper, permissions).Editor
keeperOrg1 := mustGenerateKeeper(t, helper, adminOrg1, nil)
keeperOrg2 := mustGenerateKeeper(t, helper, adminOrg2, nil)
keeperOrgA := mustGenerateKeeper(t, helper, editorOrgA, nil)
keeperOrgB := mustGenerateKeeper(t, helper, editorOrgB, nil)
clientOrg1 := helper.GetResourceClient(apis.ResourceClientArgs{User: adminOrg1, GVR: gvrKeepers})
clientOrg2 := helper.GetResourceClient(apis.ResourceClientArgs{User: adminOrg2, GVR: gvrKeepers})
clientOrgA := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgA, GVR: gvrKeepers})
clientOrgB := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgB, GVR: gvrKeepers})
// Create
t.Run("creating a keeper with the same name as one from another namespace does not return an error", func(t *testing.T) {
// Org1 creating a keeper with the same name from Org2.
// OrgA creating a keeper with the same name from OrgB.
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
testData.SetName(keeperOrg2.GetName())
testData.SetName(keeperOrgB.GetName())
raw, err := clientOrg1.Resource.Create(ctx, testData, metav1.CreateOptions{})
raw, err := clientOrgA.Resource.Create(ctx, testData, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
// Org2 creating a keeper with the same name from Org1.
// OrgA creating a keeper with the same name from OrgB.
testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
testData.SetName(keeperOrg1.GetName())
testData.SetName(keeperOrgA.GetName())
raw, err = clientOrg2.Resource.Create(ctx, testData, metav1.CreateOptions{})
raw, err = clientOrgB.Resource.Create(ctx, testData, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
require.NoError(t, clientOrg1.Resource.Delete(ctx, keeperOrg2.GetName(), metav1.DeleteOptions{}))
require.NoError(t, clientOrg2.Resource.Delete(ctx, keeperOrg1.GetName(), metav1.DeleteOptions{}))
require.NoError(t, clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{}))
require.NoError(t, clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{}))
})
// Read
t.Run("fetching a keeper from another namespace returns not found", func(t *testing.T) {
var statusErr *apierrors.StatusError
// Org1 trying to fetch keeper from Org2.
raw, err := clientOrg1.Resource.Get(ctx, keeperOrg2.GetName(), metav1.GetOptions{})
// OrgA trying to fetch keeper from OrgB.
raw, err := clientOrgA.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
// Org2 trying to fetch keeper from Org1.
raw, err = clientOrg2.Resource.Get(ctx, keeperOrg1.GetName(), metav1.GetOptions{})
// OrgB trying to fetch keeper from OrgA.
raw, err = clientOrgB.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
@ -319,23 +362,23 @@ func TestIntegrationKeeper(t *testing.T) {
t.Run("updating a keeper from another namespace returns not found", func(t *testing.T) {
var statusErr *apierrors.StatusError
// Org1 trying to update securevalue from Org2.
// OrgA trying to update securevalue from OrgB.
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
testData.SetName(keeperOrg2.GetName())
testData.SetName(keeperOrgB.GetName())
testData.Object["spec"].(map[string]any)["title"] = "New title"
raw, err := clientOrg1.Resource.Update(ctx, testData, metav1.UpdateOptions{})
raw, err := clientOrgA.Resource.Update(ctx, testData, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
// Org2 trying to update keeper from Org1.
// OrgB trying to update keeper from OrgA.
testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
testData.SetName(keeperOrg1.GetName())
testData.SetName(keeperOrgA.GetName())
testData.Object["spec"].(map[string]any)["title"] = "New title"
raw, err = clientOrg2.Resource.Update(ctx, testData, metav1.UpdateOptions{})
raw, err = clientOrgB.Resource.Update(ctx, testData, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
@ -344,40 +387,243 @@ func TestIntegrationKeeper(t *testing.T) {
// Delete
t.Run("deleting a keeper from another namespace does not return an error but does not delete it", func(t *testing.T) {
// Org1 trying to delete keeper from Org2.
err := clientOrg1.Resource.Delete(ctx, keeperOrg2.GetName(), metav1.DeleteOptions{})
// OrgA trying to delete keeper from OrgB.
err := clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{})
require.NoError(t, err)
// Check that it still exists from the perspective of Org2.
raw, err := clientOrg2.Resource.Get(ctx, keeperOrg2.GetName(), metav1.GetOptions{})
// Check that it still exists from the perspective of OrgB.
raw, err := clientOrgB.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
// Org2 trying to delete keeper from Org1.
err = clientOrg2.Resource.Delete(ctx, keeperOrg1.GetName(), metav1.DeleteOptions{})
// OrgB trying to delete keeper from OrgA.
err = clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{})
require.NoError(t, err)
// Check that it still exists from the perspective of Org1.
raw, err = clientOrg1.Resource.Get(ctx, keeperOrg1.GetName(), metav1.GetOptions{})
// Check that it still exists from the perspective of OrgA.
raw, err = clientOrgA.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
})
// List
t.Run("listing keeper from a namespace does not return the ones from another namespace", func(t *testing.T) {
// Org1 listing keeper.
listOrg1, err := clientOrg1.Resource.List(ctx, metav1.ListOptions{})
// OrgA listing keeper.
listOrgA, err := clientOrgA.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, listOrgA)
require.Len(t, listOrgA.Items, 1)
require.Equal(t, *keeperOrgA, listOrgA.Items[0])
// OrgB listing keeper.
listOrgB, err := clientOrgB.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, listOrgB)
require.Len(t, listOrgB.Items, 1)
require.Equal(t, *keeperOrgB, listOrgB.Items[0])
})
})
t.Run("keeper actions without having required permissions", func(t *testing.T) {
// Create users on a random org without specifying secrets-related permissions.
editorP := mustCreateUsers(t, helper, nil).Editor
clientP := helper.GetResourceClient(apis.ResourceClientArgs{
User: editorP,
GVR: gvrKeepers,
})
// GET
rawGet, err := clientP.Resource.Get(ctx, "some-keeper", metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, rawGet)
var statusGetErr *apierrors.StatusError
require.True(t, errors.As(err, &statusGetErr))
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code)
// LIST
rawList, err := clientP.Resource.List(ctx, metav1.ListOptions{})
require.Error(t, err)
require.Nil(t, rawList)
var statusListErr *apierrors.StatusError
require.True(t, errors.As(err, &statusListErr))
require.EqualValues(t, http.StatusForbidden, statusListErr.Status().Code)
// CREATE
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") // to pass validation before authz.
rawCreate, err := clientP.Resource.Create(ctx, testKeeper, metav1.CreateOptions{})
require.Error(t, err)
require.Nil(t, rawCreate)
var statusCreateErr *apierrors.StatusError
require.True(t, errors.As(err, &statusCreateErr))
require.EqualValues(t, http.StatusForbidden, statusCreateErr.Status().Code)
// UPDATE
testKeeper.SetName("test") // to pass validation before authz.
rawUpdate, err := clientP.Resource.Update(ctx, testKeeper, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, rawUpdate)
var statusUpdateErr *apierrors.StatusError
require.True(t, errors.As(err, &statusUpdateErr))
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code)
// DELETE
err = clientP.Resource.Delete(ctx, "some-keeper", metav1.DeleteOptions{})
require.Error(t, err)
var statusDeleteErr *apierrors.StatusError
require.True(t, errors.As(err, &statusDeleteErr))
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code)
})
t.Run("keeper actions with permissions but with limited scope", func(t *testing.T) {
suffix := strconv.FormatInt(rand.Int64(), 10)
// Fix the Keeper names.
keeperName := "kp-" + suffix
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml")
testKeeper.SetName(keeperName)
keeperNameAnother := "kp-another-" + suffix
testKeeperAnother := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml")
testKeeperAnother.SetName(keeperNameAnother)
// Fix the org ID because we will create another user with scope "all" permissions on the same org, to compare.
orgID := rand.Int64() + 2
// Permissions which allow any action, but scoped actions (get, update, delete) only on `keeperName` and NO OTHER Keeper.
scopedLimitedPermissions := map[string]ResourcePermission{
ResourceKeepers: {
Actions: ActionsAllKeepers,
Name: keeperName,
},
}
// Create users (+ client) with permission to manage ONLY the Keeper `keeperName`.
editorLimited := mustCreateUsersWithOrg(t, helper, orgID, scopedLimitedPermissions).Editor
clientScopedLimited := helper.GetResourceClient(apis.ResourceClientArgs{
User: editorLimited,
GVR: gvrKeepers,
})
// Create users (+ client) with permission to manage ANY Keepers.
scopedAllPermissions := map[string]ResourcePermission{
ResourceKeepers: {
Actions: ActionsAllKeepers,
Name: "*", // this or not sending a `Name` have the same effect.
},
}
editorAll := mustCreateUsersWithOrg(t, helper, orgID, scopedAllPermissions).Editor
clientScopedAll := helper.GetResourceClient(apis.ResourceClientArgs{
User: editorAll,
GVR: gvrKeepers,
})
t.Run("CREATE", func(t *testing.T) {
// Create the Keeper with the limited client.
rawCreateLimited, err := clientScopedLimited.Resource.Create(ctx, testKeeper, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, rawCreateLimited)
// Create another Keeper with the limited client, because the action is not scoped (no `name` available).
rawCreateLimited, err = clientScopedLimited.Resource.Create(ctx, testKeeperAnother, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, rawCreateLimited)
})
t.Run("READ", func(t *testing.T) {
// Retrieve `keeperName` from the limited client.
rawGetLimited, err := clientScopedLimited.Resource.Get(ctx, keeperName, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, rawGetLimited)
require.Equal(t, rawGetLimited.GetUID(), rawGetLimited.GetUID())
// Retrieve `keeperName` from the scope-all client.
rawGetAll, err := clientScopedAll.Resource.Get(ctx, keeperName, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, rawGetAll)
require.Equal(t, rawGetAll.GetUID(), rawGetLimited.GetUID())
// Even though we can create it, we cannot retrieve `keeperNameAnother` from the limited client.
rawGetLimited, err = clientScopedLimited.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, rawGetLimited)
var statusGetErr *apierrors.StatusError
require.True(t, errors.As(err, &statusGetErr))
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code)
// Retrieve `keeperNameAnother` from the scope-all client.
rawGetAll, err = clientScopedAll.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, rawGetAll)
})
t.Run("LIST", func(t *testing.T) {
// List Keepers from the limited client should return only 1, but it doesn't.
rawList, err := clientScopedLimited.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, rawList)
require.Len(t, rawList.Items, 1) // TODO: Can view both Keepers. How can we limit that?
// List Keepers from the scope-all client should return all of them.
rawList, err = clientScopedAll.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, rawList)
require.Len(t, rawList.Items, 2)
})
t.Run("UPDATE", func(t *testing.T) {
// Update `keeperName` from the limited client.
testKeeperUpdate := testKeeper.DeepCopy()
testKeeperUpdate.Object["spec"].(map[string]any)["title"] = "keeper-title-1234"
rawUpdate, err := clientScopedLimited.Resource.Update(ctx, testKeeperUpdate, metav1.UpdateOptions{})
require.NoError(t, err)
require.NotNil(t, rawUpdate)
// Try to update `keeperNameAnother` from the limited client.
testKeeperAnotherUpdate := testKeeperAnother.DeepCopy()
testKeeperAnotherUpdate.Object["spec"].(map[string]any)["title"] = "keeper-title-5678"
rawUpdate, err = clientScopedLimited.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, rawUpdate)
var statusUpdateErr *apierrors.StatusError
require.True(t, errors.As(err, &statusUpdateErr))
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code)
// Update `keeperNameAnother` from the scope-all client.
rawUpdate, err = clientScopedAll.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{})
require.NoError(t, err)
require.NotNil(t, rawUpdate)
})
// Keep this last for cleaning up the resources.
t.Run("DELETE", func(t *testing.T) {
// Try to delete `keeperNameAnother` from the limited client.
err := clientScopedLimited.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{})
require.Error(t, err)
var statusDeleteErr *apierrors.StatusError
require.True(t, errors.As(err, &statusDeleteErr))
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code)
// Delete `keeperNameAnother` from the scope-all client.
err = clientScopedAll.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{})
require.NoError(t, err)
require.NotNil(t, listOrg1)
require.Len(t, listOrg1.Items, 1)
require.Equal(t, *keeperOrg1, listOrg1.Items[0])
// Org2 listing keeper.
listOrg2, err := clientOrg2.Resource.List(ctx, metav1.ListOptions{})
// Delete `keeperName` from the limited client.
err = clientScopedLimited.Resource.Delete(ctx, keeperName, metav1.DeleteOptions{})
require.NoError(t, err)
require.NotNil(t, listOrg2)
require.Len(t, listOrg2.Items, 1)
require.Equal(t, *keeperOrg2, listOrg2.Items[0])
})
})
}

@ -1,11 +1,18 @@
package secret
import (
"cmp"
"context"
"encoding/json"
"math/rand/v2"
"strconv"
"testing"
"github.com/grafana/grafana/pkg/registry/apis/secret"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
@ -14,6 +21,29 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
var (
ResourceSecureValues = "secret.securevalues"
ResourceKeepers = "secret.keepers"
ActionsAllKeepers = []string{
secret.ActionSecretKeepersCreate,
secret.ActionSecretKeepersWrite,
secret.ActionSecretKeepersRead,
secret.ActionSecretKeepersDelete,
}
ActionsAllSecureValues = []string{
secret.ActionSecretSecureValuesCreate,
secret.ActionSecretSecureValuesWrite,
secret.ActionSecretSecureValuesRead,
secret.ActionSecretSecureValuesDelete,
}
)
type ResourcePermission struct {
Actions []string
Name string // empty or "*" for all
}
func TestMain(m *testing.M) {
testsuite.Run(m)
}
@ -54,6 +84,42 @@ func TestIntegrationDiscoveryClient(t *testing.T) {
})
}
func mustCreateUsersWithOrg(t *testing.T, helper *apis.K8sTestHelper, orgID int64, permissionMap map[string]ResourcePermission) apis.OrgUsers {
t.Helper()
permissions := make([]resourcepermissions.SetResourcePermissionCommand, 0, len(permissionMap))
for resource, permission := range permissionMap {
permissions = append(permissions, resourcepermissions.SetResourcePermissionCommand{
Actions: permission.Actions,
Resource: resource,
ResourceAttribute: "uid",
ResourceID: cmp.Or(permission.Name, "*"),
})
}
orgName := "org-" + strconv.FormatInt(orgID, 10)
userSuffix := strconv.FormatInt(rand.Int64(), 10)
// Add here admin or viewer if necessary.
editor := helper.CreateUser("editor-"+userSuffix, orgName, org.RoleEditor, permissions)
staff := helper.CreateTeam("staff-"+userSuffix, "staff-"+userSuffix+"@"+orgName, editor.Identity.GetOrgID())
// Also call this method for each new user.
helper.AddOrUpdateTeamMember(editor, staff.ID, team.PermissionTypeMember)
return apis.OrgUsers{
Editor: editor,
Staff: staff,
}
}
func mustCreateUsers(t *testing.T, helper *apis.K8sTestHelper, permissionMap map[string]ResourcePermission) apis.OrgUsers {
orgID := rand.Int64() + 2 // if it is 0, becomes 2 and not 1.
return mustCreateUsersWithOrg(t, helper, orgID, permissionMap)
}
func mustGenerateSecureValue(t *testing.T, helper *apis.K8sTestHelper, user apis.User, keeperName string) *unstructured.Unstructured {
t.Helper()
@ -61,7 +127,6 @@ func mustGenerateSecureValue(t *testing.T, helper *apis.K8sTestHelper, user apis
t.Cleanup(cancel)
secureValueClient := helper.GetResourceClient(apis.ResourceClientArgs{
// #TODO: figure out permissions topic
User: user,
GVR: gvrSecureValues,
})
@ -87,7 +152,6 @@ func mustGenerateKeeper(t *testing.T, helper *apis.K8sTestHelper, user apis.User
t.Cleanup(cancel)
keeperClient := helper.GetResourceClient(apis.ResourceClientArgs{
// #TODO: figure out permissions topic
User: user,
GVR: gvrKeepers,
})

@ -3,7 +3,9 @@ package secret
import (
"context"
"errors"
"math/rand/v2"
"net/http"
"strconv"
"strings"
"testing"
@ -14,6 +16,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
@ -30,6 +33,9 @@ func TestIntegrationSecureValue(t *testing.T) {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // required for experimental APIs
EnableFeatureToggles: []string{
@ -39,18 +45,27 @@ func TestIntegrationSecureValue(t *testing.T) {
},
})
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
permissions := map[string]ResourcePermission{
ResourceSecureValues: {Actions: ActionsAllSecureValues},
// in order to create securevalues, we need to first create keepers (and delete them to clean it up).
ResourceKeepers: {
Actions: []string{
secret.ActionSecretKeepersCreate,
secret.ActionSecretKeepersDelete,
},
},
}
genericUserEditor := mustCreateUsers(t, helper, permissions).Editor
client := helper.GetResourceClient(apis.ResourceClientArgs{
// #TODO: figure out permissions topic
User: helper.Org1.Admin,
User: genericUserEditor,
GVR: gvrSecureValues,
})
t.Run("creating a secure value returns it without any of the value or ref", func(t *testing.T) {
keeper := mustGenerateKeeper(t, helper, helper.Org1.Admin, nil)
raw := mustGenerateSecureValue(t, helper, helper.Org1.Admin, keeper.GetName())
keeper := mustGenerateKeeper(t, helper, genericUserEditor, nil)
raw := mustGenerateSecureValue(t, helper, genericUserEditor, keeper.GetName())
secureValue := new(secretv0alpha1.SecureValue)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, secureValue)
@ -94,7 +109,7 @@ func TestIntegrationSecureValue(t *testing.T) {
})
t.Run("and updating the secure value replaces the spec fields and returns them", func(t *testing.T) {
newKeeper := mustGenerateKeeper(t, helper, helper.Org1.Admin, nil)
newKeeper := mustGenerateKeeper(t, helper, genericUserEditor, nil)
newRaw := helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
newRaw.SetName(raw.GetName())
@ -118,8 +133,8 @@ func TestIntegrationSecureValue(t *testing.T) {
})
t.Run("creating a secure value with a `value` then updating it to a `ref` returns an error", func(t *testing.T) {
keeper := mustGenerateKeeper(t, helper, helper.Org1.Admin, nil)
svWithValue := mustGenerateSecureValue(t, helper, helper.Org1.Admin, keeper.GetName())
keeper := mustGenerateKeeper(t, helper, genericUserEditor, nil)
svWithValue := mustGenerateSecureValue(t, helper, genericUserEditor, keeper.GetName())
testData := svWithValue.DeepCopy()
testData.Object["spec"].(map[string]any)["value"] = nil
@ -160,7 +175,7 @@ func TestIntegrationSecureValue(t *testing.T) {
t.Run("deleting a secure value that exists does not return an error", func(t *testing.T) {
generatePrefix := "generated-"
keeper := mustGenerateKeeper(t, helper, helper.Org1.Admin, nil)
keeper := mustGenerateKeeper(t, helper, genericUserEditor, nil)
testData := helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
testData.SetGenerateName(generatePrefix)
@ -195,55 +210,60 @@ func TestIntegrationSecureValue(t *testing.T) {
})
t.Run("creating securevalues in multiple namespaces", func(t *testing.T) {
adminOrg1 := helper.Org1.Admin
adminOrg2 := helper.OrgB.Admin
permissions := map[string]ResourcePermission{
ResourceSecureValues: {Actions: ActionsAllSecureValues},
ResourceKeepers: {Actions: ActionsAllKeepers},
}
keeperOrg1 := mustGenerateKeeper(t, helper, adminOrg1, nil)
keeperOrg2 := mustGenerateKeeper(t, helper, adminOrg2, nil)
editorOrgA := mustCreateUsers(t, helper, permissions).Editor
editorOrgB := mustCreateUsers(t, helper, permissions).Editor
secureValueOrg1 := mustGenerateSecureValue(t, helper, adminOrg1, keeperOrg1.GetName())
secureValueOrg2 := mustGenerateSecureValue(t, helper, adminOrg2, keeperOrg2.GetName())
keeperOrgA := mustGenerateKeeper(t, helper, editorOrgA, nil)
keeperOrgB := mustGenerateKeeper(t, helper, editorOrgB, nil)
clientOrg1 := helper.GetResourceClient(apis.ResourceClientArgs{User: adminOrg1, GVR: gvrSecureValues})
clientOrg2 := helper.GetResourceClient(apis.ResourceClientArgs{User: adminOrg2, GVR: gvrSecureValues})
secureValueOrgA := mustGenerateSecureValue(t, helper, editorOrgA, keeperOrgA.GetName())
secureValueOrgB := mustGenerateSecureValue(t, helper, editorOrgB, keeperOrgB.GetName())
clientOrgA := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgA, GVR: gvrSecureValues})
clientOrgB := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgB, GVR: gvrSecureValues})
// Create
t.Run("creating a securevalue with the same name as one from another namespace does not return an error", func(t *testing.T) {
// Org1 creating a securevalue with the same name from Org2.
// OrgA creating a securevalue with the same name from OrgB.
testData := helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
testData.SetName(secureValueOrg2.GetName())
testData.Object["spec"].(map[string]any)["keeper"] = keeperOrg1.GetName()
testData.SetName(secureValueOrgB.GetName())
testData.Object["spec"].(map[string]any)["keeper"] = keeperOrgA.GetName()
raw, err := clientOrg1.Resource.Create(ctx, testData, metav1.CreateOptions{})
raw, err := clientOrgA.Resource.Create(ctx, testData, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
// Org2 creating a securevalue with the same name from Org1.
// OrgB creating a securevalue with the same name from OrgA.
testData = helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
testData.SetName(secureValueOrg1.GetName())
testData.Object["spec"].(map[string]any)["keeper"] = keeperOrg2.GetName()
testData.SetName(secureValueOrgA.GetName())
testData.Object["spec"].(map[string]any)["keeper"] = keeperOrgB.GetName()
raw, err = clientOrg2.Resource.Create(ctx, testData, metav1.CreateOptions{})
raw, err = clientOrgB.Resource.Create(ctx, testData, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
require.NoError(t, clientOrg1.Resource.Delete(ctx, secureValueOrg2.GetName(), metav1.DeleteOptions{}))
require.NoError(t, clientOrg2.Resource.Delete(ctx, secureValueOrg1.GetName(), metav1.DeleteOptions{}))
require.NoError(t, clientOrgA.Resource.Delete(ctx, secureValueOrgB.GetName(), metav1.DeleteOptions{}))
require.NoError(t, clientOrgB.Resource.Delete(ctx, secureValueOrgA.GetName(), metav1.DeleteOptions{}))
})
// Read
t.Run("fetching a securevalue from another namespace returns not found", func(t *testing.T) {
var statusErr *apierrors.StatusError
// Org1 trying to fetch securevalue from Org2.
raw, err := clientOrg1.Resource.Get(ctx, secureValueOrg2.GetName(), metav1.GetOptions{})
// OrgA trying to fetch securevalue from OrgB.
raw, err := clientOrgA.Resource.Get(ctx, secureValueOrgB.GetName(), metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
// Org2 trying to fetch securevalue from Org1.
raw, err = clientOrg2.Resource.Get(ctx, secureValueOrg1.GetName(), metav1.GetOptions{})
// OrgB trying to fetch securevalue from OrgA.
raw, err = clientOrgB.Resource.Get(ctx, secureValueOrgA.GetName(), metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
@ -254,23 +274,23 @@ func TestIntegrationSecureValue(t *testing.T) {
t.Run("updating a securevalue from another namespace returns not found", func(t *testing.T) {
var statusErr *apierrors.StatusError
// Org1 trying to update securevalue from Org2.
// OrgA trying to update securevalue from OrgB.
testData := helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
testData.SetName(secureValueOrg2.GetName())
testData.SetName(secureValueOrgB.GetName())
testData.Object["spec"].(map[string]any)["title"] = "New title"
raw, err := clientOrg1.Resource.Update(ctx, testData, metav1.UpdateOptions{})
raw, err := clientOrgA.Resource.Update(ctx, testData, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
// Org2 trying to update securevalue from Org1.
// OrgB trying to update securevalue from OrgA.
testData = helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
testData.SetName(secureValueOrg1.GetName())
testData.SetName(secureValueOrgA.GetName())
testData.Object["spec"].(map[string]any)["title"] = "New title"
raw, err = clientOrg2.Resource.Update(ctx, testData, metav1.UpdateOptions{})
raw, err = clientOrgB.Resource.Update(ctx, testData, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, raw)
require.True(t, errors.As(err, &statusErr))
@ -279,40 +299,255 @@ func TestIntegrationSecureValue(t *testing.T) {
// Delete
t.Run("deleting a securevalue from another namespace does not return an error but does not delete it", func(t *testing.T) {
// Org1 trying to delete securevalue from Org2.
err := clientOrg1.Resource.Delete(ctx, secureValueOrg2.GetName(), metav1.DeleteOptions{})
// OrgA trying to delete securevalue from OrgB.
err := clientOrgA.Resource.Delete(ctx, secureValueOrgB.GetName(), metav1.DeleteOptions{})
require.NoError(t, err)
// Check that it still exists from the perspective of Org2.
raw, err := clientOrg2.Resource.Get(ctx, secureValueOrg2.GetName(), metav1.GetOptions{})
// Check that it still exists from the perspective of OrgB.
raw, err := clientOrgB.Resource.Get(ctx, secureValueOrgB.GetName(), metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
// Org2 trying to delete securevalue from Org1.
err = clientOrg2.Resource.Delete(ctx, secureValueOrg1.GetName(), metav1.DeleteOptions{})
// OrgB trying to delete securevalue from OrgA.
err = clientOrgB.Resource.Delete(ctx, secureValueOrgA.GetName(), metav1.DeleteOptions{})
require.NoError(t, err)
// Check that it still exists from the perspective of Org1.
raw, err = clientOrg1.Resource.Get(ctx, secureValueOrg1.GetName(), metav1.GetOptions{})
// Check that it still exists from the perspective of OrgA.
raw, err = clientOrgA.Resource.Get(ctx, secureValueOrgA.GetName(), metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, raw)
})
// List
t.Run("listing securevalues from a namespace does not return the ones from another namespace", func(t *testing.T) {
// Org1 listing securevalues.
listOrg1, err := clientOrg1.Resource.List(ctx, metav1.ListOptions{})
// OrgA listing securevalues.
listOrgA, err := clientOrgA.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, listOrgA)
require.Len(t, listOrgA.Items, 1)
require.Equal(t, *secureValueOrgA, listOrgA.Items[0])
// OrgB listing securevalues.
listOrgB, err := clientOrgB.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, listOrgB)
require.Len(t, listOrgB.Items, 1)
require.Equal(t, *secureValueOrgB, listOrgB.Items[0])
})
})
t.Run("securevalue actions without having required permissions", func(t *testing.T) {
// Create users on a random org without specifying secrets-related permissions.
editorP := mustCreateUsers(t, helper, nil).Editor
clientP := helper.GetResourceClient(apis.ResourceClientArgs{
User: editorP,
GVR: gvrSecureValues,
})
// GET
rawGet, err := clientP.Resource.Get(ctx, "some-securevalue", metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, rawGet)
var statusGetErr *apierrors.StatusError
require.True(t, errors.As(err, &statusGetErr))
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code)
// LIST
rawList, err := clientP.Resource.List(ctx, metav1.ListOptions{})
require.Error(t, err)
require.Nil(t, rawList)
var statusListErr *apierrors.StatusError
require.True(t, errors.As(err, &statusListErr))
require.EqualValues(t, http.StatusForbidden, statusListErr.Status().Code)
// CREATE
testSecureValue := helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml") // to pass validation before authz.
rawCreate, err := clientP.Resource.Create(ctx, testSecureValue, metav1.CreateOptions{})
require.Error(t, err)
require.Nil(t, rawCreate)
var statusCreateErr *apierrors.StatusError
require.True(t, errors.As(err, &statusCreateErr))
require.EqualValues(t, http.StatusForbidden, statusCreateErr.Status().Code)
// UPDATE
testSecureValue.SetName("test") // to pass validation before authz.
rawUpdate, err := clientP.Resource.Update(ctx, testSecureValue, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, rawUpdate)
var statusUpdateErr *apierrors.StatusError
require.True(t, errors.As(err, &statusUpdateErr))
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code)
// DELETE
err = clientP.Resource.Delete(ctx, "some-securevalue", metav1.DeleteOptions{})
require.Error(t, err)
var statusDeleteErr *apierrors.StatusError
require.True(t, errors.As(err, &statusDeleteErr))
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code)
})
t.Run("securevalue actions with permissions but with limited scope", func(t *testing.T) {
suffix := strconv.FormatInt(rand.Int64(), 10)
// Fix the SecureValue names.
secureValueName := "sv-" + suffix
testSecureValue := helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
testSecureValue.SetName(secureValueName)
secureValueNameAnother := "sv-another-" + suffix
testSecureValueAnother := helper.LoadYAMLOrJSONFile("testdata/secure-value-generate.yaml")
testSecureValueAnother.SetName(secureValueNameAnother)
// Fix the org ID because we will create another user with scope "all" permissions on the same org, to compare.
orgID := rand.Int64() + 2
// Permissions which allow any action, but scoped actions (get, update, delete) only on `secureValueName` and NO OTHER SecureValue.
scopedLimitedPermissions := map[string]ResourcePermission{
ResourceSecureValues: {
Actions: ActionsAllSecureValues,
Name: secureValueName,
},
// we need to have a Keeper before creating SecureValues.
ResourceKeepers: {
Actions: []string{
secret.ActionSecretKeepersCreate,
secret.ActionSecretKeepersDelete,
},
},
}
// Create users (+ client) with permission to manage ONLY the SecureValue `secureValueName`.
editorLimited := mustCreateUsersWithOrg(t, helper, orgID, scopedLimitedPermissions).Editor
clientScopedLimited := helper.GetResourceClient(apis.ResourceClientArgs{
User: editorLimited,
GVR: gvrSecureValues,
})
// Create users (+ client) with permission to manage ANY SecureValues.
scopedAllPermissions := map[string]ResourcePermission{
ResourceSecureValues: {
Actions: ActionsAllSecureValues,
Name: "*", // this or not sending a `Name` have the same effect.
},
}
editorAll := mustCreateUsersWithOrg(t, helper, orgID, scopedAllPermissions).Editor
clientScopedAll := helper.GetResourceClient(apis.ResourceClientArgs{
User: editorAll,
GVR: gvrSecureValues,
})
// Create an initial Keeper to be able to start creating SecureValues.
keeper := mustGenerateKeeper(t, helper, editorLimited, nil)
testSecureValue.Object["spec"].(map[string]any)["keeper"] = keeper.GetName()
testSecureValueAnother.Object["spec"].(map[string]any)["keeper"] = keeper.GetName()
t.Run("CREATE", func(t *testing.T) {
// Create the SecureValue with the limited client.
rawCreateLimited, err := clientScopedLimited.Resource.Create(ctx, testSecureValue, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, rawCreateLimited)
// Create another SecureValue with the limited client, because the action is not scoped (no `name` available).
rawCreateLimited, err = clientScopedLimited.Resource.Create(ctx, testSecureValueAnother, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, rawCreateLimited)
})
t.Run("READ", func(t *testing.T) {
// Retrieve `secureValueName` from the limited client.
rawGetLimited, err := clientScopedLimited.Resource.Get(ctx, secureValueName, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, rawGetLimited)
require.Equal(t, rawGetLimited.GetUID(), rawGetLimited.GetUID())
// Retrieve `secureValueName` from the scope-all client.
rawGetAll, err := clientScopedAll.Resource.Get(ctx, secureValueName, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, rawGetAll)
require.Equal(t, rawGetAll.GetUID(), rawGetLimited.GetUID())
// Even though we can create it, we cannot retrieve `secureValueNameAnother` from the limited client.
rawGetLimited, err = clientScopedLimited.Resource.Get(ctx, secureValueNameAnother, metav1.GetOptions{})
require.Error(t, err)
require.Nil(t, rawGetLimited)
var statusGetErr *apierrors.StatusError
require.True(t, errors.As(err, &statusGetErr))
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code)
// Retrieve `secureValueNameAnother` from the scope-all client.
rawGetAll, err = clientScopedAll.Resource.Get(ctx, secureValueNameAnother, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, rawGetAll)
})
t.Run("LIST", func(t *testing.T) {
// List SecureValues from the limited client should return only 1, but it doesn't.
rawList, err := clientScopedLimited.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, rawList)
require.Len(t, rawList.Items, 1) // TODO: Can view both SecureValues. How can we limit that?
// List SecureValues from the scope-all client should return all of them.
rawList, err = clientScopedAll.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.NotNil(t, rawList)
require.Len(t, rawList.Items, 2)
})
t.Run("UPDATE", func(t *testing.T) {
// Update `secureValueName` from the limited client.
testSecureValueUpdate := testSecureValue.DeepCopy()
testSecureValueUpdate.Object["spec"].(map[string]any)["title"] = "sv-title-1234"
rawUpdate, err := clientScopedLimited.Resource.Update(ctx, testSecureValueUpdate, metav1.UpdateOptions{})
require.NoError(t, err)
require.NotNil(t, rawUpdate)
// Try to update `secureValueNameAnother` from the limited client.
testSecureValueAnotherUpdate := testSecureValueAnother.DeepCopy()
testSecureValueAnotherUpdate.Object["spec"].(map[string]any)["title"] = "sv-title-5678"
rawUpdate, err = clientScopedLimited.Resource.Update(ctx, testSecureValueAnotherUpdate, metav1.UpdateOptions{})
require.Error(t, err)
require.Nil(t, rawUpdate)
var statusUpdateErr *apierrors.StatusError
require.True(t, errors.As(err, &statusUpdateErr))
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code)
// Update `secureValueNameAnother` from the scope-all client.
rawUpdate, err = clientScopedAll.Resource.Update(ctx, testSecureValueAnotherUpdate, metav1.UpdateOptions{})
require.NoError(t, err)
require.NotNil(t, rawUpdate)
})
// Keep this last for cleaning up the resources.
t.Run("DELETE", func(t *testing.T) {
// Try to delete `secureValueNameAnother` from the limited client.
err := clientScopedLimited.Resource.Delete(ctx, secureValueNameAnother, metav1.DeleteOptions{})
require.Error(t, err)
var statusDeleteErr *apierrors.StatusError
require.True(t, errors.As(err, &statusDeleteErr))
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code)
// Delete `secureValueNameAnother` from the scope-all client.
err = clientScopedAll.Resource.Delete(ctx, secureValueNameAnother, metav1.DeleteOptions{})
require.NoError(t, err)
require.NotNil(t, listOrg1)
require.Len(t, listOrg1.Items, 1)
require.Equal(t, *secureValueOrg1, listOrg1.Items[0])
// Org2 listing securevalues.
listOrg2, err := clientOrg2.Resource.List(ctx, metav1.ListOptions{})
// Delete `secureValueName` from the limited client.
err = clientScopedLimited.Resource.Delete(ctx, secureValueName, metav1.DeleteOptions{})
require.NoError(t, err)
require.NotNil(t, listOrg2)
require.Len(t, listOrg2.Items, 1)
require.Equal(t, *secureValueOrg2, listOrg2.Items[0])
})
})
}

Loading…
Cancel
Save