From b6226c6173dccf659b7fbf43a6e6c7e21b151ffe Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 27 Aug 2025 15:00:09 +0200 Subject: [PATCH] `grafana-iam`: Skeleton of the resource permission api backend (#110218) * Extract from #108753 Co-Authored-By: mohammad-hamid * Tackle create Co-Authored-By: mohammad-hamid * WIP use identity store to resolve role names * Commit empty service for now * Clean * For now only show name and created at --------- Co-authored-by: mohammad-hamid --- apps/iam/pkg/apis/iam/v0alpha1/register.go | 26 +++++ pkg/registry/apis/iam/authorizer.go | 1 + pkg/registry/apis/iam/models.go | 10 +- pkg/registry/apis/iam/register.go | 38 ++++--- .../apis/iam/resourcepermission/models.go | 11 ++ .../apis/iam/resourcepermission/sql.go | 11 ++ .../iam/resourcepermission/storage_backend.go | 103 ++++++++++++++++++ .../apis/iam/resourcepermission/templates.go | 11 ++ 8 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 pkg/registry/apis/iam/resourcepermission/models.go create mode 100644 pkg/registry/apis/iam/resourcepermission/sql.go create mode 100644 pkg/registry/apis/iam/resourcepermission/storage_backend.go create mode 100644 pkg/registry/apis/iam/resourcepermission/templates.go diff --git a/apps/iam/pkg/apis/iam/v0alpha1/register.go b/apps/iam/pkg/apis/iam/v0alpha1/register.go index 3e0d4561290..c46776fb0c2 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/register.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/register.go @@ -72,6 +72,30 @@ var RoleInfo = utils.NewResourceInfo(GROUP, VERSION, }, ) +var ResourcePermissionInfo = utils.NewResourceInfo(GROUP, VERSION, + "resourcepermissions", "resourcepermission", "ResourcePermission", + func() runtime.Object { return &ResourcePermission{} }, + func() runtime.Object { return &ResourcePermissionList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + perm, ok := obj.(*ResourcePermission) + if ok { + if perm != nil { + return []interface{}{ + perm.Name, + perm.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + } + return nil, fmt.Errorf("expected resource permission") + }, + }, +) + var ( SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder @@ -90,6 +114,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &CoreRoleList{}, &Role{}, &RoleList{}, + &ResourcePermission{}, + &ResourcePermissionList{}, // What is this about? &metav1.PartialObjectMetadata{}, diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 7b55746ad0e..1645f7b5a3a 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -33,6 +33,7 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth authorizer := gfauthorizer.NewResourceAuthorizer(accessClient) resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = authorizer resourceAuthorizer[iamv0.RoleInfo.GetName()] = authorizer + resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = authorizer return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer} } diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index ac3c21643af..fc1b3de6017 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -27,9 +27,10 @@ type RoleStorageBackend interface{ resource.StorageBackend } // This is used just so wire has something unique to return type IdentityAccessManagementAPIBuilder struct { // Stores - store legacy.LegacyIdentityStore - coreRolesStorage CoreRoleStorageBackend - rolesStorage RoleStorageBackend + store legacy.LegacyIdentityStore + coreRolesStorage CoreRoleStorageBackend + rolesStorage RoleStorageBackend + resourcePermissionsStorage resource.StorageBackend // Access Control authorizer authorizer.Authorizer @@ -47,7 +48,8 @@ type IdentityAccessManagementAPIBuilder struct { sso ssosettings.Service // Toggle for enabling authz management apis - enableAuthZApis bool + enableAuthZApis bool + enableResourcePermissionApis bool // Toggle for enabling authn mutation enableAuthnMutation bool diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 658133797d1..5f0e326b215 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -29,6 +29,7 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" "github.com/grafana/grafana/pkg/registry/apis/iam/sso" "github.com/grafana/grafana/pkg/registry/apis/iam/team" @@ -53,23 +54,26 @@ func RegisterAPIService( coreRolesStorage CoreRoleStorageBackend, rolesStorage RoleStorageBackend, ) (*IdentityAccessManagementAPIBuilder, error) { - store := legacy.NewLegacySQLStores(legacysql.NewDatabaseProvider(sql)) + dbProvider := legacysql.NewDatabaseProvider(sql) + store := legacy.NewLegacySQLStores(dbProvider) legacyAccessClient := newLegacyAccessClient(ac, store) authorizer := newIAMAuthorizer(accessClient, legacyAccessClient) builder := &IdentityAccessManagementAPIBuilder{ - store: store, - coreRolesStorage: coreRolesStorage, - rolesStorage: rolesStorage, - sso: ssoService, - authorizer: authorizer, - legacyAccessClient: legacyAccessClient, - accessClient: accessClient, - display: user.NewLegacyDisplayREST(store), - reg: reg, - enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis), - enableAuthnMutation: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation), - enableDualWriter: true, + store: store, + coreRolesStorage: coreRolesStorage, + rolesStorage: rolesStorage, + resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), + sso: ssoService, + authorizer: authorizer, + legacyAccessClient: legacyAccessClient, + accessClient: accessClient, + display: user.NewLegacyDisplayREST(store), + reg: reg, + enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis), + enableResourcePermissionApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis), + enableAuthnMutation: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation), + enableDualWriter: true, } apiregistration.RegisterAPI(builder) @@ -173,6 +177,14 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[iamv0.RoleInfo.StoragePath()] = roleStore } + if b.enableResourcePermissionApis { + resourcePermissionStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage) + if err != nil { + return err + } + storage[iamv0.ResourcePermissionInfo.StoragePath()] = resourcePermissionStore + } + apiGroupInfo.VersionedResourcesStorageMap[legacyiamv0.VERSION] = storage return nil } diff --git a/pkg/registry/apis/iam/resourcepermission/models.go b/pkg/registry/apis/iam/resourcepermission/models.go new file mode 100644 index 00000000000..7a70d9893a7 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/models.go @@ -0,0 +1,11 @@ +package resourcepermission + +import ( + "errors" + "fmt" +) + +var ( + errNotImplemented = errors.New("not supported by this storage backend") + errEmptyName = fmt.Errorf("name cannot be empty") +) diff --git a/pkg/registry/apis/iam/resourcepermission/sql.go b/pkg/registry/apis/iam/resourcepermission/sql.go new file mode 100644 index 00000000000..c0790370280 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/sql.go @@ -0,0 +1,11 @@ +package resourcepermission + +// List + +// Get + +// Create + +// Update + +// Delete diff --git a/pkg/registry/apis/iam/resourcepermission/storage_backend.go b/pkg/registry/apis/iam/resourcepermission/storage_backend.go new file mode 100644 index 00000000000..32f83e9b1db --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/storage_backend.go @@ -0,0 +1,103 @@ +package resourcepermission + +import ( + "context" + "fmt" + "iter" + "net/http" + "sync" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +var ( + _ resource.StorageBackend = &ResourcePermSqlBackend{} +) + +type ResourcePermSqlBackend struct { + dbProvider legacysql.LegacyDatabaseProvider + logger log.Logger + + subscribers []chan *resource.WrittenEvent + mutex sync.Mutex +} + +func ProvideStorageBackend(dbProvider legacysql.LegacyDatabaseProvider) *ResourcePermSqlBackend { + return &ResourcePermSqlBackend{ + dbProvider: dbProvider, + logger: log.New("resourceperm_storage_backend"), + + subscribers: make([]chan *resource.WrittenEvent, 0), + mutex: sync.Mutex{}, + } +} + +func (s *ResourcePermSqlBackend) GetResourceStats(ctx context.Context, namespace string, minCount int) ([]resource.ResourceStats, error) { + return []resource.ResourceStats{}, errNotImplemented +} + +func (s *ResourcePermSqlBackend) ListHistory(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) { + return 0, errNotImplemented +} + +func (s *ResourcePermSqlBackend) ListIterator(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) { + return 0, errNotImplemented +} + +func (s *ResourcePermSqlBackend) ListModifiedSince(ctx context.Context, key resource.NamespacedResource, sinceRv int64) (int64, iter.Seq2[*resource.ModifiedResource, error]) { + return 0, func(yield func(*resource.ModifiedResource, error) bool) { + yield(nil, errNotImplemented) + } +} + +func (s *ResourcePermSqlBackend) ReadResource(_ context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse { + return &resource.BackendReadResponse{ + Key: req.GetKey(), + Error: &resourcepb.ErrorResult{Code: http.StatusForbidden, Message: errNotImplemented.Error()}, + } +} + +func (s *ResourcePermSqlBackend) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) { + stream := make(chan *resource.WrittenEvent, 10) + return stream, nil +} + +func isValidKey(key *resourcepb.ResourceKey, requireName bool) error { + gr := v0alpha1.ResourcePermissionInfo.GroupResource() + if key.Group != gr.Group { + return fmt.Errorf("expecting group (%s != %s)", key.Group, gr.Group) + } + if key.Resource != gr.Resource { + return fmt.Errorf("expecting resource (%s != %s)", key.Resource, gr.Resource) + } + if requireName && key.Name == "" { + return fmt.Errorf("expecting name (uid): %w", errEmptyName) + } + return nil +} + +func (s *ResourcePermSqlBackend) WriteEvent(ctx context.Context, event resource.WriteEvent) (rv int64, err error) { + ns, err := types.ParseNamespace(event.Key.Namespace) + if err != nil { + return 0, err + } + if ns.OrgID <= 0 { + return 0, apierrors.NewBadRequest("write requires a valid namespace") + } + + if err := isValidKey(event.Key, true); err != nil { + return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid key %q: %v", event.Key, err.Error())) + } + + switch event.Type { + default: + return 0, fmt.Errorf("unsupported event type: %v", event.Type) + } +} diff --git a/pkg/registry/apis/iam/resourcepermission/templates.go b/pkg/registry/apis/iam/resourcepermission/templates.go new file mode 100644 index 00000000000..c0790370280 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/templates.go @@ -0,0 +1,11 @@ +package resourcepermission + +// List + +// Get + +// Create + +// Update + +// Delete