From 3d543a336f7108a5f3381dd17e0db27aa223e1b6 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 26 Jun 2025 10:11:28 +0200 Subject: [PATCH] IAM: Register CoreRole apis (#106924) * IAM: Register CoreRole apis * one line store instantiation * Small refactor for readability * Add authorizer for CoreRole * Nit * Error strings should not end with punctiation * Account for error * Switch to use the local resource client * error should not start with upper casing * noopStorageErr should have a name starting with err * Update workspace * I don't know why I don't have the same output as the CI :shrug: * Dependency xOwnership * imports * Import order * Rename alias to make it clear this is legacy --- apps/iam/go.mod | 5 +- apps/iam/go.sum | 2 + apps/iam/pkg/apis/iam/v0alpha1/register.go | 73 ++++++++++ go.mod | 1 + go.sum | 2 + pkg/apimachinery/identity/context.go | 1 + pkg/registry/apis/iam/authorizer.go | 47 ++++++- pkg/registry/apis/iam/models.go | 44 ++++++ .../apis/iam/noopstorage/storage_backend.go | 56 ++++++++ pkg/registry/apis/iam/register.go | 128 ++++++++++++------ pkg/registry/apis/wireset.go | 8 ++ pkg/services/authz/rbac/mapper.go | 3 +- 12 files changed, 323 insertions(+), 47 deletions(-) create mode 100644 apps/iam/pkg/apis/iam/v0alpha1/register.go create mode 100644 pkg/registry/apis/iam/models.go create mode 100644 pkg/registry/apis/iam/noopstorage/storage_backend.go diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 9035467c0e1..811abf007e5 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -4,6 +4,7 @@ go 1.24.4 require ( github.com/grafana/grafana-app-sdk v0.39.0 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.33.1 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -33,11 +34,13 @@ require ( github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 043c32160bc..c61276e084f 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -36,6 +36,8 @@ github.com/grafana/grafana-app-sdk v0.39.0 h1:WC2E9BKXWDX/e2bajdAFjQEyyWf9BFp7Yz github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/apps/iam/pkg/apis/iam/v0alpha1/register.go b/apps/iam/pkg/apis/iam/v0alpha1/register.go new file mode 100644 index 00000000000..c2a3fb14b1a --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/register.go @@ -0,0 +1,73 @@ +package v0alpha1 + +import ( + "fmt" + "time" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + GROUP = "iam.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION +) + +var CoreRoleInfo = utils.NewResourceInfo(GROUP, VERSION, + "coreroles", "corerole", "CoreRole", + func() runtime.Object { return &CoreRole{} }, + func() runtime.Object { return &CoreRoleList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "Core role name"}, // Not sure this is actually needed + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + core, ok := obj.(*CoreRole) + if ok { + if core != nil { + return []interface{}{ + core.Name, + core.Spec.Title, + core.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + } + return nil, fmt.Errorf("expected core role") + }, + }, +) + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme + schemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} +) + +func init() { + localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(schemeGroupVersion, + &CoreRole{}, + &CoreRoleList{}, + + // What is this about? + &metav1.PartialObjectMetadata{}, + &metav1.PartialObjectMetadataList{}, + ) + metav1.AddToGroupVersion(scheme, schemeGroupVersion) + return nil +} + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + // return RegisterDefaults(scheme) + return nil +} diff --git a/go.mod b/go.mod index 47a343a8077..e7960695a20 100644 --- a/go.mod +++ b/go.mod @@ -212,6 +212,7 @@ require ( github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250527064921-326081cdb7a1 // @grafana/alerting-backend github.com/grafana/grafana/apps/dashboard v0.0.0-20250616215703-eb4de388c3cf // @grafana/grafana-app-platform-squad @grafana/dashboards-squad github.com/grafana/grafana/apps/folder v0.0.0-20250527064921-326081cdb7a1 // @grafana/grafana-search-and-storage + github.com/grafana/grafana/apps/iam v0.0.0-20250620082852-de59956db4df // @grafana/identity-access-team github.com/grafana/grafana/apps/investigations v0.0.0-20250527064921-326081cdb7a1 // @fcjack @matryer github.com/grafana/grafana/apps/playlist v0.0.0-20250527064921-326081cdb7a1 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/aggregator v0.0.0-20250527064921-326081cdb7a1 // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index d509342f667..a672bd0eaf4 100644 --- a/go.sum +++ b/go.sum @@ -1621,6 +1621,8 @@ github.com/grafana/grafana/apps/dashboard v0.0.0-20250616215703-eb4de388c3cf h1: github.com/grafana/grafana/apps/dashboard v0.0.0-20250616215703-eb4de388c3cf/go.mod h1:XcJIUxPfKAhDb9mx9jHeV9U2xVA2WgE93ukd4Bx8U/M= github.com/grafana/grafana/apps/folder v0.0.0-20250527064921-326081cdb7a1 h1:SkVUcSlXIUi46gSGfe5/Xqn2p51c/unmkZRDX+274Os= github.com/grafana/grafana/apps/folder v0.0.0-20250527064921-326081cdb7a1/go.mod h1:z9u5VFG9q1CcIt62c9RIP8dEWHX81NdiPCjtpKKeuFU= +github.com/grafana/grafana/apps/iam v0.0.0-20250620082852-de59956db4df h1:t0y8BiEJ7dBq2/eYQI66kYyOGYdPCxNPNftrRwL6kIo= +github.com/grafana/grafana/apps/iam v0.0.0-20250620082852-de59956db4df/go.mod h1:6j/luBu5kqJysChmy9bnlCDYvukIAxqaoUEVI+HaCuA= github.com/grafana/grafana/apps/investigations v0.0.0-20250527064921-326081cdb7a1 h1:IN+KiBwtvJ3JCLdsPyAmGKJupWckH3h5iXpELd78rPo= github.com/grafana/grafana/apps/investigations v0.0.0-20250527064921-326081cdb7a1/go.mod h1:/uuCYrNS3VOTZ7gTD+cBHD9WBIYhqj7bK6EnX3TP1wg= github.com/grafana/grafana/apps/playlist v0.0.0-20250527064921-326081cdb7a1 h1:GNPPHXhUv++4qKdrUJg/Mfi/HJPGlc5u+/FjElYAcOM= diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 58ab1085d36..be5d7fe83fa 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -125,6 +125,7 @@ var serviceIdentityTokenPermissions = getTokenPermissions( "dashboard.grafana.app", "secret.grafana.app", "query.grafana.app", + "iam.grafana.app", ) var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{ diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 3f0d71e1bb9..3b7ed2839ab 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -7,18 +7,53 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" authlib "github.com/grafana/authlib/types" + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" - iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" ) -func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIdentityStore) (authorizer.Authorizer, authlib.AccessClient) { +type iamAuthorizer struct { + resourceAuthorizer map[string]authorizer.Authorizer // Map resource to its authorizer +} + +func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient authlib.AccessClient) authorizer.Authorizer { + resourceAuthorizer := make(map[string]authorizer.Authorizer) + + // Identity specific resources + legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient) + resourceAuthorizer[legacyiamv0.UserResourceInfo.GetName()] = legacyAuthorizer + resourceAuthorizer[legacyiamv0.ServiceAccountResourceInfo.GetName()] = legacyAuthorizer + resourceAuthorizer[legacyiamv0.TeamResourceInfo.GetName()] = legacyAuthorizer + resourceAuthorizer["display"] = legacyAuthorizer + + // Access specific resources + authorizer := gfauthorizer.NewResourceAuthorizer(accessClient) + resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = authorizer + + return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer} +} + +func (s *iamAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + authz, ok := s.resourceAuthorizer[attr.GetResource()] + if !ok { + return authorizer.DecisionDeny, "", fmt.Errorf("no authorizer found for resource %s", attr.GetResource()) + } + + return authz.Authorize(ctx, attr) +} + +func newLegacyAccessClient(ac accesscontrol.AccessControl, store legacy.LegacyIdentityStore) authlib.AccessClient { client := accesscontrol.NewLegacyAccessClient( ac, accesscontrol.ResourceAuthorizerOptions{ - Resource: iamv0.UserResourceInfo.GetName(), + Resource: legacyiamv0.UserResourceInfo.GetName(), Attr: "id", Mapping: map[string]string{ utils.VerbGet: accesscontrol.ActionOrgUsersRead, @@ -42,7 +77,7 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden }, }, accesscontrol.ResourceAuthorizerOptions{ - Resource: iamv0.ServiceAccountResourceInfo.GetName(), + Resource: legacyiamv0.ServiceAccountResourceInfo.GetName(), Attr: "id", Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns authlib.NamespaceInfo, name string) ([]string, error) { res, err := store.GetServiceAccountInternalID(ctx, ns, legacy.GetServiceAccountInternalIDQuery{ @@ -55,7 +90,7 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden }), }, accesscontrol.ResourceAuthorizerOptions{ - Resource: iamv0.TeamResourceInfo.GetName(), + Resource: legacyiamv0.TeamResourceInfo.GetName(), Attr: "id", Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns authlib.NamespaceInfo, name string) ([]string, error) { res, err := store.GetTeamInternalID(ctx, ns, legacy.GetTeamInternalIDQuery{ @@ -69,5 +104,5 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden }, ) - return gfauthorizer.NewResourceAuthorizer(client), client + return client } diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go new file mode 100644 index 00000000000..4332054cafd --- /dev/null +++ b/pkg/registry/apis/iam/models.go @@ -0,0 +1,44 @@ +package iam + +import ( + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/registry/apis/iam/user" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/ssosettings" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/prometheus/client_golang/prometheus" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil) +var _ builder.APIGroupRouteProvider = (*IdentityAccessManagementAPIBuilder)(nil) + +// CoreRoleStorageBackend uses the resource.StorageBackend interface to provide storage for core roles. +// Used wire to identify the storage backend for core roles. +type CoreRoleStorageBackend interface{ resource.StorageBackend } + +// This is used just so wire has something unique to return +type IdentityAccessManagementAPIBuilder struct { + // Stores + store legacy.LegacyIdentityStore + coreRolesStorage CoreRoleStorageBackend + + // Access Control + authorizer authorizer.Authorizer + // legacyAccessClient is used for the identity apis, we need to migrate to the access client + legacyAccessClient types.AccessClient + // accessClient is used for the core role apis + accessClient types.AccessClient + + reg prometheus.Registerer + + // non-k8s api route + display *user.LegacyDisplayREST + + // Not set for multi-tenant deployment for now + sso ssosettings.Service + + // Toggle for enabling authz management apis + enableAuthZApis bool +} diff --git a/pkg/registry/apis/iam/noopstorage/storage_backend.go b/pkg/registry/apis/iam/noopstorage/storage_backend.go new file mode 100644 index 00000000000..200d0b5c485 --- /dev/null +++ b/pkg/registry/apis/iam/noopstorage/storage_backend.go @@ -0,0 +1,56 @@ +package noopstorage + +import ( + "context" + "errors" + "net/http" + + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +var ( + _ resource.StorageBackend = &StorageBackendImpl{} + + errNoopStorage = errors.New("unavailable functionality") +) + +type StorageBackendImpl struct{} + +func ProvideStorageBackend() *StorageBackendImpl { + return &StorageBackendImpl{} +} + +// GetResourceStats implements resource.StorageBackend. +func (c *StorageBackendImpl) GetResourceStats(ctx context.Context, namespace string, minCount int) ([]resource.ResourceStats, error) { + return []resource.ResourceStats{}, errNoopStorage +} + +// ListHistory implements resource.StorageBackend. +func (c *StorageBackendImpl) ListHistory(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) { + return 0, errNoopStorage +} + +// ListIterator implements resource.StorageBackend. +func (c *StorageBackendImpl) ListIterator(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) { + return 0, errNoopStorage +} + +// ReadResource implements resource.StorageBackend. +func (c *StorageBackendImpl) ReadResource(_ context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse { + return &resource.BackendReadResponse{ + Key: req.GetKey(), + Error: &resourcepb.ErrorResult{Code: http.StatusForbidden, Message: errNoopStorage.Error()}, + } +} + +// WatchWriteEvents implements resource.StorageBackend. +func (c *StorageBackendImpl) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) { + stream := make(chan *resource.WrittenEvent, 10) + return stream, nil +} + +// WriteEvent implements resource.StorageBackend. +func (c *StorageBackendImpl) WriteEvent(context.Context, resource.WriteEvent) (int64, error) { + return 0, errNoopStorage +} diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 3a24a81d5cb..7e3a54f9ee1 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -4,10 +4,12 @@ import ( "context" "strings" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" @@ -15,8 +17,12 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" "github.com/grafana/authlib/types" + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + 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/serviceaccount" @@ -25,41 +31,37 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/user" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/apistore" + "github.com/grafana/grafana/pkg/storage/unified/resource" ) -var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil) -var _ builder.APIGroupRouteProvider = (*IdentityAccessManagementAPIBuilder)(nil) - -// This is used just so wire has something unique to return -type IdentityAccessManagementAPIBuilder struct { - store legacy.LegacyIdentityStore - authorizer authorizer.Authorizer - accessClient types.AccessClient - - // non-k8s api route - display *user.LegacyDisplayREST - - // Not set for multi-tenant deployment for now - sso ssosettings.Service -} - func RegisterAPIService( + features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, ssoService ssosettings.Service, sql db.DB, ac accesscontrol.AccessControl, + accessClient types.AccessClient, + reg prometheus.Registerer, + coreRolesStorage CoreRoleStorageBackend, ) (*IdentityAccessManagementAPIBuilder, error) { store := legacy.NewLegacySQLStores(legacysql.NewDatabaseProvider(sql)) - authorizer, client := newLegacyAuthorizer(ac, store) + legacyAccessClient := newLegacyAccessClient(ac, store) + authorizer := newIAMAuthorizer(accessClient, legacyAccessClient) builder := &IdentityAccessManagementAPIBuilder{ - store: store, - sso: ssoService, - authorizer: authorizer, - accessClient: client, - display: user.NewLegacyDisplayREST(store), + store: store, + coreRolesStorage: coreRolesStorage, + sso: ssoService, + authorizer: authorizer, + legacyAccessClient: legacyAccessClient, + accessClient: accessClient, + display: user.NewLegacyDisplayREST(store), + reg: reg, + enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis), } apiregistration.RegisterAPI(builder) @@ -85,54 +87,80 @@ func NewAPIService(store legacy.LegacyIdentityStore) *IdentityAccessManagementAP } func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersion { - return iamv0.SchemeGroupVersion + return legacyiamv0.SchemeGroupVersion } func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - iamv0.AddKnownTypes(scheme, iamv0.VERSION) + if b.enableAuthZApis { + if err := iamv0.AddToScheme(scheme); err != nil { + return err + } + } + + legacyiamv0.AddKnownTypes(scheme, legacyiamv0.VERSION) // Link this version to the internal representation. // This is used for server-side-apply (PATCH), and avoids the error: // "no kind is registered for the type" - iamv0.AddKnownTypes(scheme, runtime.APIVersionInternal) + legacyiamv0.AddKnownTypes(scheme, runtime.APIVersionInternal) - metav1.AddToGroupVersion(scheme, iamv0.SchemeGroupVersion) - return scheme.SetVersionPriority(iamv0.SchemeGroupVersion) + metav1.AddToGroupVersion(scheme, legacyiamv0.SchemeGroupVersion) + return scheme.SetVersionPriority(legacyiamv0.SchemeGroupVersion) } func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string { return []string{builder.AllResourcesAllowed} } -func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { +func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { storage := map[string]rest.Storage{} - teamResource := iamv0.TeamResourceInfo - storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store, b.accessClient) + teamResource := legacyiamv0.TeamResourceInfo + storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store, b.legacyAccessClient) storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) - teamBindingResource := iamv0.TeamBindingResourceInfo + teamBindingResource := legacyiamv0.TeamBindingResourceInfo storage[teamBindingResource.StoragePath()] = team.NewLegacyBindingStore(b.store) - userResource := iamv0.UserResourceInfo - storage[userResource.StoragePath()] = user.NewLegacyStore(b.store, b.accessClient) + userResource := legacyiamv0.UserResourceInfo + storage[userResource.StoragePath()] = user.NewLegacyStore(b.store, b.legacyAccessClient) storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store) - serviceAccountResource := iamv0.ServiceAccountResourceInfo - storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.accessClient) + serviceAccountResource := legacyiamv0.ServiceAccountResourceInfo + storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.legacyAccessClient) storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) if b.sso != nil { - ssoResource := iamv0.SSOSettingResourceInfo + ssoResource := legacyiamv0.SSOSettingResourceInfo storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso) } - apiGroupInfo.VersionedResourcesStorageMap[iamv0.VERSION] = storage + if b.enableAuthZApis { + // v0alpha1 + store, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage) + if err != nil { + return err + } + storage[iamv0.CoreRoleInfo.StoragePath()] = store + } + + apiGroupInfo.VersionedResourcesStorageMap[legacyiamv0.VERSION] = storage return nil } func (b *IdentityAccessManagementAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return iamv0.GetOpenAPIDefinitions + defs := legacyiamv0.GetOpenAPIDefinitions + if b.enableAuthZApis { + defs = func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + def1 := legacyiamv0.GetOpenAPIDefinitions(ref) + def2 := iamv0.GetOpenAPIDefinitions(ref) + for k, v := range def2 { + def1[k] = v + } + return def1 + } + } + return defs } func (b *IdentityAccessManagementAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { @@ -195,3 +223,25 @@ func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes(gv schema.GroupVersion func (b *IdentityAccessManagementAPIBuilder) GetAuthorizer() authorizer.Authorizer { return b.authorizer } + +func NewLocalStore(resourceInfo utils.ResourceInfo, scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, + reg prometheus.Registerer, ac types.AccessClient, storageBackend resource.StorageBackend) (grafanarest.Storage, error) { + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ + Backend: storageBackend, + Reg: reg, + AccessClient: ac, + }) + if err != nil { + return nil, err + } + defaultOpts, err := defaultOptsGetter.GetRESTOptions(resourceInfo.GroupResource(), nil) + if err != nil { + return nil, err + } + + client := resource.NewLocalResourceClient(server) + optsGetter := apistore.NewRESTOptionsGetterForClient(client, defaultOpts.StorageConfig.Config, nil) + + store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter) + return store, err +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index aef1aa79913..e79ccf45811 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" + "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" "github.com/grafana/grafana/pkg/registry/apis/query" @@ -30,6 +31,12 @@ var ProvisioningExtras = wire.NewSet( MergeProvisioningExtras, ) +// WireSetExts is a set of providers that can be overridden by enterprise implementations. +var WireSetExts = wire.NewSet( + noopstorage.ProvideStorageBackend, + wire.Bind(new(iam.CoreRoleStorageBackend), new(*noopstorage.StorageBackendImpl)), +) + var WireSet = wire.NewSet( ProvideRegistryServiceSink, // dummy background service that forces registration @@ -44,6 +51,7 @@ var WireSet = wire.NewSet( featuretoggle.RegisterAPIService, datasource.RegisterAPIService, folders.RegisterAPIService, + WireSetExts, // this will be moved to wireexts_oss.go in a following PR iam.RegisterAPIService, ProvisioningExtras, provisioning.RegisterAPIService, diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go index e279c7f453b..7017f1f086c 100644 --- a/pkg/services/authz/rbac/mapper.go +++ b/pkg/services/authz/rbac/mapper.go @@ -102,7 +102,8 @@ func NewMapperRegistry() MapperRegistry { "folders": newResourceTranslation("folders", "uid", true), }, "iam.grafana.app": { - "teams": newResourceTranslation("teams", "id", false), + "teams": newResourceTranslation("teams", "id", false), + "coreroles": newResourceTranslation("roles", "uid", false), }, "secret.grafana.app": { "securevalues": newResourceTranslation("secret.securevalues", "uid", false),