diff --git a/apps/iam/kinds/role.cue b/apps/iam/kinds/role.cue index 1911fe83616..e308bb175b7 100644 --- a/apps/iam/kinds/role.cue +++ b/apps/iam/kinds/role.cue @@ -25,6 +25,7 @@ coreroleKind: { globalroleKind: { kind: "GlobalRole" pluralName: "GlobalRoles" + scope: "Cluster" codegen: { ts: { enabled: false } go: { enabled: true } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go index be220cb0e95..7ebbb0ceadf 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go @@ -11,7 +11,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaGlobalRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewGlobalRole(), &GlobalRoleList{}, resource.WithKind("GlobalRole"), - resource.WithPlural("globalroles"), resource.WithScope(resource.NamespacedScope)) + resource.WithPlural("globalroles"), resource.WithScope(resource.ClusterScope)) kindGlobalRole = resource.Kind{ Schema: schemaGlobalRole, Codecs: map[resource.KindEncoding]resource.Codec{ diff --git a/apps/iam/pkg/apis/iam/v0alpha1/register.go b/apps/iam/pkg/apis/iam/v0alpha1/register.go index 05dbc30e684..ddb053d0818 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/register.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/register.go @@ -74,6 +74,32 @@ var RoleInfo = utils.NewResourceInfo(GROUP, VERSION, }, ) +var GlobalRoleInfo = utils.NewResourceInfo(GROUP, VERSION, + "globalroles", "globalrole", "GlobalRole", + func() runtime.Object { return &GlobalRole{} }, + func() runtime.Object { return &GlobalRoleList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Group", Type: "string", Format: "group", Description: "Role group"}, + {Name: "Title", Type: "string", Format: "string", Description: "Role name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + globalRole, ok := obj.(*GlobalRole) + if ok { + return []interface{}{ + globalRole.Name, + globalRole.Spec.Group, + globalRole.Spec.Title, + globalRole.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected global role") + }, + }, +) + var ResourcePermissionInfo = utils.NewResourceInfo(GROUP, VERSION, "resourcepermissions", "resourcepermission", "ResourcePermission", func() runtime.Object { return &ResourcePermission{} }, @@ -308,6 +334,13 @@ func AddResourcePermissionKnownTypes(scheme *runtime.Scheme, version schema.Grou return nil } +func AddGlobalRoleKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &GlobalRole{}, + &GlobalRoleList{}, + ) + return nil +} func AddAuthNKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, // Identity diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 664c12226de..db7352f05b6 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -18,6 +18,8 @@ import ( v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" ) +var () + var appManifestData = app.ManifestData{ AppName: "iam", Group: "iam.grafana.app", @@ -30,7 +32,7 @@ var appManifestData = app.ManifestData{ { Kind: "GlobalRole", Plural: "GlobalRoles", - Scope: "Namespaced", + Scope: "Cluster", Conversion: false, }, diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f1b1ce4154d..77413b2454c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -966,6 +966,10 @@ export interface FeatureToggles { */ kubernetesAuthzRolesApi?: boolean; /** + * Registers AuthZ Global Roles /apis endpoint + */ + kubernetesAuthzGlobalRolesApi?: boolean; + /** * Registers AuthZ Role Bindings /apis endpoint */ kubernetesAuthzRoleBindingsApi?: boolean; diff --git a/pkg/registry/apis/iam/api_installer.go b/pkg/registry/apis/iam/api_installer.go index 1d456dd3777..8e7e5dc8701 100644 --- a/pkg/registry/apis/iam/api_installer.go +++ b/pkg/registry/apis/iam/api_installer.go @@ -39,6 +39,9 @@ type ApiInstaller[T runtime.Object] interface { // while keeping the core IAM registration logic in OSS. type RoleApiInstaller ApiInstaller[*iamv0.Role] +// GlobalRoleApiInstaller provides GlobalRole-specific API registration and validation. +type GlobalRoleApiInstaller ApiInstaller[*iamv0.GlobalRole] + // NoopApiInstaller is a no-op implementation for when roles are not available (OSS). type NoopApiInstaller[T runtime.Object] struct { ResourceInfo utils.ResourceInfo @@ -77,3 +80,10 @@ func ProvideNoopRoleApiInstaller() RoleApiInstaller { ResourceInfo: iamv0.RoleInfo, } } + +// ProvideNoopGlobalRoleApiInstaller provides a no-op global role installer specifically for GlobalRole types. +func ProvideNoopGlobalRoleApiInstaller() GlobalRoleApiInstaller { + return &NoopApiInstaller[*iamv0.GlobalRole]{ + ResourceInfo: iamv0.GlobalRoleInfo, + } +} diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index 9e4d8e484e4..5e2c8589b61 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -57,6 +57,7 @@ type IdentityAccessManagementAPIBuilder struct { ssoLegacyStore *sso.LegacyStore coreRolesStorage CoreRoleStorageBackend roleApiInstaller RoleApiInstaller + globalRoleApiInstaller GlobalRoleApiInstaller resourcePermissionsStorage resource.StorageBackend roleBindingsStorage RoleBindingStorageBackend externalGroupMappingStorage ExternalGroupMappingStorageBackend diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 7f42d620987..bf29142648f 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -219,12 +219,20 @@ func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Schem enableCoreRolesApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzCoreRolesApi, false, openfeature.TransactionContext(ctx)) enableRolesApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzRolesApi, false, openfeature.TransactionContext(ctx)) enableRoleBindingsApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzRoleBindingsApi, false, openfeature.TransactionContext(ctx)) + enableGlobalRolesApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzGlobalRolesApi, false, openfeature.TransactionContext(ctx)) if enableCoreRolesApi || enableRolesApi || enableRoleBindingsApi { if err := iamv0.AddAuthZKnownTypes(scheme); err != nil { return err } } + + if enableGlobalRolesApi { + if err := iamv0.AddGlobalRoleKnownTypes(scheme); err != nil { + return err + } + } + //nolint:staticcheck // not yet migrated to OpenFeature if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) { if err := iamv0.AddResourcePermissionKnownTypes(scheme, iamv0.SchemeGroupVersion); err != nil { @@ -264,6 +272,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge enableCoreRolesApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzCoreRolesApi, false, openfeature.TransactionContext(ctx)) enableRolesApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzRolesApi, false, openfeature.TransactionContext(ctx)) enableRoleBindingsApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzRoleBindingsApi, false, openfeature.TransactionContext(ctx)) + enableGlobalRolesApi := client.Boolean(ctx, featuremgmt.FlagKubernetesAuthzGlobalRolesApi, false, openfeature.TransactionContext(ctx)) // teams + users must have shorter names because they are often used as part of another name opts.StorageOptsRegister(iamv0.TeamResourceInfo.GroupResource(), apistore.StorageOptions{ @@ -313,6 +322,12 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } } + if enableGlobalRolesApi { + if err := b.globalRoleApiInstaller.RegisterStorage(apiGroupInfo, &opts, storage); err != nil { + return err + } + } + if enableRoleBindingsApi { if err := b.UpdateRoleBindingsAPIGroup(apiGroupInfo, opts, storage, enableZanzanaSync); err != nil { return err @@ -656,6 +671,8 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm return externalgroupmapping.ValidateOnCreate(typedObj) case *iamv0.Role: return b.roleApiInstaller.ValidateOnCreate(ctx, typedObj) + case *iamv0.GlobalRole: + return b.globalRoleApiInstaller.ValidateOnCreate(ctx, typedObj) } return nil case admission.Update: @@ -686,12 +703,20 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm return fmt.Errorf("expected old object to be a Role, got %T", oldRoleObj) } return b.roleApiInstaller.ValidateOnUpdate(ctx, oldRoleObj, typedObj) + case *iamv0.GlobalRole: + oldGlobalRoleObj, ok := a.GetOldObject().(*iamv0.GlobalRole) + if !ok { + return fmt.Errorf("expected old object to be a GlobalRole, got %T", oldGlobalRoleObj) + } + return b.globalRoleApiInstaller.ValidateOnUpdate(ctx, oldGlobalRoleObj, typedObj) } return nil case admission.Delete: - switch oldRoleObj := a.GetOldObject().(type) { + switch oldObj := a.GetOldObject().(type) { case *iamv0.Role: - return b.roleApiInstaller.ValidateOnDelete(ctx, oldRoleObj) + return b.roleApiInstaller.ValidateOnDelete(ctx, oldObj) + case *iamv0.GlobalRole: + return b.globalRoleApiInstaller.ValidateOnDelete(ctx, oldObj) } return nil case admission.Connect: diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 296b7d25b59..f7ce9026090 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -29,6 +29,7 @@ var WireSetExts = wire.NewSet( noopstorage.ProvideStorageBackend, wire.Bind(new(iam.CoreRoleStorageBackend), new(*noopstorage.StorageBackendImpl)), iam.ProvideNoopRoleApiInstaller, + iam.ProvideNoopGlobalRoleApiInstaller, wire.Bind(new(iam.RoleBindingStorageBackend), new(*noopstorage.StorageBackendImpl)), wire.Bind(new(iam.ExternalGroupMappingStorageBackend), new(*noopstorage.StorageBackendImpl)), diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c9f2ca03184..37a85003f41 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1596,6 +1596,13 @@ var ( Owner: identityAccessTeam, HideFromDocs: true, }, + { + Name: "kubernetesAuthzGlobalRolesApi", + Description: "Registers AuthZ Global Roles /apis endpoint", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, + }, { Name: "kubernetesAuthzRoleBindingsApi", Description: "Registers AuthZ Role Bindings /apis endpoint", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 7557323e43b..c8ab9b7951c 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -219,6 +219,7 @@ kubernetesAuthzResourcePermissionApis,experimental,@grafana/identity-access-team kubernetesAuthzZanzanaSync,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthzCoreRolesApi,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthzRolesApi,experimental,@grafana/identity-access-team,false,false,false +kubernetesAuthzGlobalRolesApi,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthzRoleBindingsApi,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,false kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 97ecf868a64..b6731bb61fb 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -650,6 +650,10 @@ const ( // Registers AuthZ Roles /apis endpoint FlagKubernetesAuthzRolesApi = "kubernetesAuthzRolesApi" + // FlagKubernetesAuthzGlobalRolesApi + // Registers AuthZ Global Roles /apis endpoint + FlagKubernetesAuthzGlobalRolesApi = "kubernetesAuthzGlobalRolesApi" + // FlagKubernetesAuthzRoleBindingsApi // Registers AuthZ Role Bindings /apis endpoint FlagKubernetesAuthzRoleBindingsApi = "kubernetesAuthzRoleBindingsApi" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 49cdbd86374..eaf038f8be9 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1979,6 +1979,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "kubernetesAuthzGlobalRolesApi", + "resourceVersion": "1768294340511", + "creationTimestamp": "2026-01-13T08:52:20Z" + }, + "spec": { + "description": "Registers AuthZ Global Roles /apis endpoint", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromDocs": true + } + }, { "metadata": { "name": "kubernetesAuthzResourcePermissionApis",