diff --git a/api/core/v2alpha1/constants.go b/api/core/v2alpha1/constants.go index 14d55441..7e37122c 100644 --- a/api/core/v2alpha1/constants.go +++ b/api/core/v2alpha1/constants.go @@ -11,6 +11,7 @@ const ( MCPNameLabel = GroupName + "/mcp-name" MCPNamespaceLabel = GroupName + "/mcp-namespace" OIDCProviderLabel = GroupName + "/oidc-provider" + TokenProviderLabel = GroupName + "/token-provider" MCPPurposeOverrideLabel = GroupName + "/purpose" // ManagedPurposeMCPPurposeOverride is used as value for the managed purpose label. It must not be modified. @@ -32,8 +33,13 @@ const ( ConditionClusterRequestReady = "ClusterRequestReady" ConditionClusterConditionsSynced = "ClusterConditionsSynced" ConditionPrefixClusterCondition = "Cluster." - ConditionPrefixOIDCAccessReady = "OIDCAccessReady." + ConditionPrefixAccessReady = "AccessReady." ConditionAllAccessReady = "AllAccessReady" ConditionAllServicesDeleted = "AllServicesDeleted" ConditionAllClusterRequestsDeleted = "AllClusterRequestsDeleted" ) + +const ( + OIDCNamePrefix = "oidc:" + TokenNamePrefix = "token:" +) diff --git a/api/core/v2alpha1/managedcontrolplane_types.go b/api/core/v2alpha1/managedcontrolplane_types.go index d9ee3c4c..733f8e16 100644 --- a/api/core/v2alpha1/managedcontrolplane_types.go +++ b/api/core/v2alpha1/managedcontrolplane_types.go @@ -3,6 +3,8 @@ package v2alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1" + commonapi "github.com/openmcp-project/openmcp-operator/api/common" ) @@ -11,28 +13,48 @@ type ManagedControlPlaneV2Spec struct { IAM IAMConfig `json:"iam"` } -type ManagedControlPlaneV2Status struct { - commonapi.Status `json:",inline"` +type IAMConfig struct { + // Tokens is a list of token-based access configurations. + // +optional + Tokens []TokenConfig `json:"tokens,omitempty"` + // OIDC is the OIDC-based access configuration. + OIDC *OIDCConfig `json:"oidc,omitempty"` +} - // Access is a mapping from OIDC provider names to secret references. - // Each referenced secret is expected to contain a 'kubeconfig' key with the kubeconfig that was generated for the respective OIDC provider for the ManagedControlPlaneV2. - // The default OIDC provider, if configured, uses the name "default" in this mapping. - // The "default" key is also used if the ClusterProvider does not support OIDC-based access and created a serviceaccount with a token instead. +type OIDCConfig struct { + // DefaultProvider is the standard OIDC provider that is enabled for all ManagedControlPlaneV2 resources. + DefaultProvider DefaultProviderConfig `json:"defaultProvider,omitempty"` + // ExtraProviders is a list of OIDC providers that should be configured for the ManagedControlPlaneV2. + // They are independent of the standard OIDC provider and in addition to it, unless it has been disabled by not specifying any role bindings. // +optional - Access map[string]commonapi.LocalObjectReference `json:"access,omitempty"` + ExtraProviders []commonapi.OIDCProviderConfig `json:"extraProviders,omitempty"` } -type IAMConfig struct { +type DefaultProviderConfig struct { // RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. // These bindings refer to the standard OIDC provider. If empty, the standard OIDC provider is disabled. // Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. // +optional RoleBindings []commonapi.RoleBindings `json:"roleBindings,omitempty"` +} - // OIDCProviders is a list of OIDC providers that should be configured for the ManagedControlPlaneV2. - // They are independent of the standard OIDC provider and in addition to it, unless it has been disabled by not specifying any role bindings. +type TokenConfig struct { + // Name is the name of this token configuration. + // It is used to generate a secret name and must be unique among all token configurations in the same ManagedControlPlaneV2. + // +kubebuilder:validation:minLength=1 + Name string `json:"name"` + clustersv1alpha1.TokenConfig `json:",inline"` +} + +type ManagedControlPlaneV2Status struct { + commonapi.Status `json:",inline"` + + // Access is a mapping from OIDC provider names to secret references. + // Each referenced secret is expected to contain a 'kubeconfig' key with the kubeconfig that was generated for the respective OIDC provider for the ManagedControlPlaneV2. + // The default OIDC provider, if configured, uses the name "default" in this mapping. + // The "default" key is also used if the ClusterProvider does not support OIDC-based access and created a serviceaccount with a token instead. // +optional - OIDCProviders []*commonapi.OIDCProviderConfig `json:"oidcProviders,omitempty"` + Access map[string]commonapi.LocalObjectReference `json:"access,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/core/v2alpha1/zz_generated.deepcopy.go b/api/core/v2alpha1/zz_generated.deepcopy.go index 64290740..0e35c951 100644 --- a/api/core/v2alpha1/zz_generated.deepcopy.go +++ b/api/core/v2alpha1/zz_generated.deepcopy.go @@ -11,7 +11,7 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IAMConfig) DeepCopyInto(out *IAMConfig) { +func (in *DefaultProviderConfig) DeepCopyInto(out *DefaultProviderConfig) { *out = *in if in.RoleBindings != nil { in, out := &in.RoleBindings, &out.RoleBindings @@ -20,17 +20,33 @@ func (in *IAMConfig) DeepCopyInto(out *IAMConfig) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.OIDCProviders != nil { - in, out := &in.OIDCProviders, &out.OIDCProviders - *out = make([]*common.OIDCProviderConfig, len(*in)) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultProviderConfig. +func (in *DefaultProviderConfig) DeepCopy() *DefaultProviderConfig { + if in == nil { + return nil + } + out := new(DefaultProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IAMConfig) DeepCopyInto(out *IAMConfig) { + *out = *in + if in.Tokens != nil { + in, out := &in.Tokens, &out.Tokens + *out = make([]TokenConfig, len(*in)) for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(common.OIDCProviderConfig) - (*in).DeepCopyInto(*out) - } + (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.OIDC != nil { + in, out := &in.OIDC, &out.OIDC + *out = new(OIDCConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IAMConfig. @@ -140,3 +156,42 @@ func (in *ManagedControlPlaneV2Status) DeepCopy() *ManagedControlPlaneV2Status { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCConfig) DeepCopyInto(out *OIDCConfig) { + *out = *in + in.DefaultProvider.DeepCopyInto(&out.DefaultProvider) + if in.ExtraProviders != nil { + in, out := &in.ExtraProviders, &out.ExtraProviders + *out = make([]common.OIDCProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCConfig. +func (in *OIDCConfig) DeepCopy() *OIDCConfig { + if in == nil { + return nil + } + out := new(OIDCConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenConfig) DeepCopyInto(out *TokenConfig) { + *out = *in + in.TokenConfig.DeepCopyInto(&out.TokenConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenConfig. +func (in *TokenConfig) DeepCopy() *TokenConfig { + if in == nil { + return nil + } + out := new(TokenConfig) + in.DeepCopyInto(out) + return out +} diff --git a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml index 29ac9008..82074a21 100644 --- a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml +++ b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanev2s.yaml @@ -49,151 +49,313 @@ spec: description: IAM contains the access management configuration for the ManagedControlPlaneV2. properties: - oidcProviders: - description: |- - OIDCProviders is a list of OIDC providers that should be configured for the ManagedControlPlaneV2. - They are independent of the standard OIDC provider and in addition to it, unless it has been disabled by not specifying any role bindings. + oidc: + description: OIDC is the OIDC-based access configuration. + properties: + defaultProvider: + description: DefaultProvider is the standard OIDC provider + that is enabled for all ManagedControlPlaneV2 resources. + properties: + roleBindings: + description: |- + RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. + These bindings refer to the standard OIDC provider. If empty, the standard OIDC provider is disabled. + Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + items: + properties: + roleRefs: + description: |- + RoleRefs is a list of (cluster) role references that the subjects should be bound to. + Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. + items: + description: RoleRef defines a reference to a + (cluster) role that should be bound to the subjects. + properties: + kind: + description: |- + Kind is the kind of the role to bind to the subjects. + It must be 'Role' or 'ClusterRole'. + enum: + - Role + - ClusterRole + type: string + name: + description: Name is the name of the role + or cluster role to bind to the subjects. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the role to bind to the subjects. + It must be set if the kind is 'Role' and may not be set if the kind is 'ClusterRole'. + type: string + required: + - kind + - name + type: object + type: array + subjects: + description: |- + Subjects is a list of subjects that should be bound to the specified roles. + The subjects' names will be prefixed with the username prefix of the OIDC provider. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + required: + - roleRefs + - subjects + type: object + type: array + type: object + extraProviders: + description: |- + ExtraProviders is a list of OIDC providers that should be configured for the ManagedControlPlaneV2. + They are independent of the standard OIDC provider and in addition to it, unless it has been disabled by not specifying any role bindings. + items: + properties: + clientID: + description: ClientID is the client ID to use for the + OIDC provider. + minLength: 1 + type: string + extraScopes: + description: ExtraScopes is a list of extra scopes that + should be requested from the OIDC provider. + items: + type: string + type: array + groupsClaim: + default: groups + description: |- + GroupsClaim is the claim in the OIDC token that contains the groups. + If empty, the default claim "groups" will be used. + type: string + issuer: + description: |- + Issuer is the issuer URL of the OIDC provider. + Must be a valid URL. + minLength: 1 + pattern: ^https?://[^\s/$.?#].[^\s]*$ + type: string + name: + description: |- + Name is the name of the OIDC provider. + May be used in k8s resources, therefore has to be a valid k8s name. + It is also used (with a ':' suffix) as prefix in k8s resources referencing users or groups from this OIDC provider. + E.g. if the name is 'example', the username 'alice' from this provider will be referenced as 'example:alice' in k8s resources. + Must be unique among all OIDC providers configured in the same environment. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + x-kubernetes-validations: + - message: '''system'' is a reserved string and may + not be used as OIDC provider name' + rule: self != "system" + roleBindings: + description: |- + RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. + Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + items: + properties: + roleRefs: + description: |- + RoleRefs is a list of (cluster) role references that the subjects should be bound to. + Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. + items: + description: RoleRef defines a reference to + a (cluster) role that should be bound to the + subjects. + properties: + kind: + description: |- + Kind is the kind of the role to bind to the subjects. + It must be 'Role' or 'ClusterRole'. + enum: + - Role + - ClusterRole + type: string + name: + description: Name is the name of the role + or cluster role to bind to the subjects. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the role to bind to the subjects. + It must be set if the kind is 'Role' and may not be set if the kind is 'ClusterRole'. + type: string + required: + - kind + - name + type: object + type: array + subjects: + description: |- + Subjects is a list of subjects that should be bound to the specified roles. + The subjects' names will be prefixed with the username prefix of the OIDC provider. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + required: + - roleRefs + - subjects + type: object + type: array + usernameClaim: + default: sub + description: |- + UsernameClaim is the claim in the OIDC token that contains the username. + If empty, the default claim "sub" will be used. + type: string + required: + - clientID + - issuer + - name + - roleBindings + type: object + type: array + type: object + tokens: + description: Tokens is a list of token-based access configurations. items: properties: - clientID: - description: ClientID is the client ID to use for the OIDC - provider. - minLength: 1 - type: string - extraScopes: - description: ExtraScopes is a list of extra scopes that - should be requested from the OIDC provider. - items: - type: string - type: array - groupsClaim: - default: groups - description: |- - GroupsClaim is the claim in the OIDC token that contains the groups. - If empty, the default claim "groups" will be used. - type: string - issuer: - description: |- - Issuer is the issuer URL of the OIDC provider. - Must be a valid URL. - minLength: 1 - pattern: ^https?://[^\s/$.?#].[^\s]*$ - type: string name: description: |- - Name is the name of the OIDC provider. - May be used in k8s resources, therefore has to be a valid k8s name. - It is also used (with a ':' suffix) as prefix in k8s resources referencing users or groups from this OIDC provider. - E.g. if the name is 'example', the username 'alice' from this provider will be referenced as 'example:alice' in k8s resources. - Must be unique among all OIDC providers configured in the same environment. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + Name is the name of this token configuration. + It is used to generate a secret name and must be unique among all token configurations in the same ManagedControlPlaneV2. type: string - x-kubernetes-validations: - - message: '''system'' is a reserved string and may not - be used as OIDC provider name' - rule: self != "system" - roleBindings: + permissions: description: |- - RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. - Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. + Permissions are the requested permissions. + If not empty, corresponding Roles and ClusterRoles will be created in the target cluster. + The created serviceaccount will be bound to the created Roles and ClusterRoles. items: properties: - roleRefs: + name: description: |- - RoleRefs is a list of (cluster) role references that the subjects should be bound to. - Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. - items: - description: RoleRef defines a reference to a (cluster) - role that should be bound to the subjects. - properties: - kind: - description: |- - Kind is the kind of the role to bind to the subjects. - It must be 'Role' or 'ClusterRole'. - enum: - - Role - - ClusterRole - type: string - name: - description: Name is the name of the role or - cluster role to bind to the subjects. - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the role to bind to the subjects. - It must be set if the kind is 'Role' and may not be set if the kind is 'ClusterRole'. - type: string - required: - - kind - - name - type: object - type: array - subjects: + Name is an optional name for the (Cluster)Role that will be created for the requested permissions. + If not set, a randomized name that is unique in the cluster will be generated. + Note that the AccessRequest will not be granted if the to-be-created (Cluster)Role already exists, but is not managed by the AccessRequest, so choose this name carefully. + type: string + namespace: description: |- - Subjects is a list of subjects that should be bound to the specified roles. - The subjects' names will be prefixed with the username prefix of the OIDC provider. + Namespace is the namespace for which the permissions are requested. + If empty, this will result in a ClusterRole, otherwise in a Role in the respective namespace. + Note that for a Role, the namespace needs to either exist or a permission to create it must be included in the requested permissions (it will be created automatically then), otherwise the request will be rejected. + type: string + rules: + description: Rules are the requested RBAC rules. items: description: |- - Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, - or a value for non-objects such as user and group names. + PolicyRule holds information that describes a policy rule, but does not contain information + about who the rule applies to or which namespace the rule applies to. properties: - apiGroup: - description: |- - APIGroup holds the API group of the referenced subject. - Defaults to "" for ServiceAccount subjects. - Defaults to "rbac.authorization.k8s.io" for User and Group subjects. - type: string - kind: + apiGroups: description: |- - Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". - If the Authorizer does not recognized the kind value, the Authorizer should report an error. - type: string - name: - description: Name of the object being referenced. - type: string - namespace: + APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of + the enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. + items: + type: string + type: array + x-kubernetes-list-type: atomic + nonResourceURLs: description: |- - Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty - the Authorizer should report an error. - type: string + NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path + Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. + Rules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resourceNames: + description: ResourceNames is an optional white + list of names that the rule applies to. An + empty set means that everything is allowed. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resources: + description: Resources is a list of resources + this rule applies to. '*' represents all resources. + items: + type: string + type: array + x-kubernetes-list-type: atomic + verbs: + description: Verbs is a list of Verbs that apply + to ALL the ResourceKinds contained in this + rule. '*' represents all verbs. + items: + type: string + type: array + x-kubernetes-list-type: atomic required: - - kind - - name + - verbs type: object - x-kubernetes-map-type: atomic type: array required: - - roleRefs - - subjects + - rules type: object type: array - usernameClaim: - default: sub - description: |- - UsernameClaim is the claim in the OIDC token that contains the username. - If empty, the default claim "sub" will be used. - type: string - required: - - clientID - - issuer - - name - - roleBindings - type: object - type: array - roleBindings: - description: |- - RoleBindings is a list of subjects with (cluster) role bindings that should be created for them. - These bindings refer to the standard OIDC provider. If empty, the standard OIDC provider is disabled. - Note that the username prefix is added automatically to the subjects' names, it must not be explicitly specified here. - items: - properties: roleRefs: - description: |- - RoleRefs is a list of (cluster) role references that the subjects should be bound to. - Note that existence of the roles is not checked and missing (cluster) roles will result in ineffective (cluster) role bindings. + description: RoleRefs are references to existing (Cluster)Roles + that should be bound to the created serviceaccount. items: description: RoleRef defines a reference to a (cluster) role that should be bound to the subjects. @@ -221,43 +383,8 @@ spec: - name type: object type: array - subjects: - description: |- - Subjects is a list of subjects that should be bound to the specified roles. - The subjects' names will be prefixed with the username prefix of the OIDC provider. - items: - description: |- - Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, - or a value for non-objects such as user and group names. - properties: - apiGroup: - description: |- - APIGroup holds the API group of the referenced subject. - Defaults to "" for ServiceAccount subjects. - Defaults to "rbac.authorization.k8s.io" for User and Group subjects. - type: string - kind: - description: |- - Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". - If the Authorizer does not recognized the kind value, the Authorizer should report an error. - type: string - name: - description: Name of the object being referenced. - type: string - namespace: - description: |- - Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty - the Authorizer should report an error. - type: string - required: - - kind - - name - type: object - x-kubernetes-map-type: atomic - type: array required: - - roleRefs - - subjects + - name type: object type: array type: object diff --git a/docs/controller/managedcontrolplane.md b/docs/controller/managedcontrolplane.md index c7bf6b2e..71cda0c3 100644 --- a/docs/controller/managedcontrolplane.md +++ b/docs/controller/managedcontrolplane.md @@ -32,31 +32,53 @@ metadata: namespace: foo spec: iam: - roleBindings: # this sets the role bindings for the default OIDC provider (no effect if none is configured) - - subjects: - - kind: User - name: john.doe@example.com - roleRefs: - - kind: ClusterRole - name: cluster-admin - oidcProviders: # here, additional OIDC providers can be configured - - name: my-oidc-provider - issuer: https://oidc.example.com - clientID: my-client-id - extraScopes: - - foo - roleBindings: - - subjects: - - kind: User - name: foo - - kind: Group - name: bar - roleRefs: - - kind: ClusterRole - name: my-cluster-role - - kind: Role - name: my-role - namespace: default + oidc: + defaultProvider: + roleBindings: # this sets the role bindings for the default OIDC provider (no effect if none is configured) + - subjects: + - kind: User + name: john.doe@example.com + roleRefs: + - kind: ClusterRole + name: cluster-admin + + extraProviders: # here, additional OIDC providers can be configured + - name: my-oidc-provider + issuer: https://oidc.example.com + clientID: my-client-id + extraScopes: + - foo + roleBindings: + - subjects: + - kind: User + name: foo + - kind: Group + name: bar + roleRefs: + - kind: ClusterRole + name: my-cluster-role + - kind: Role + name: my-role + namespace: default + + tokens: # here, static tokens can be configured + - name: admin # this token will be named 'admin' and must be unique per MCP + # roleRefs and permissions can be either set individually or together + roleRefs: # this sets the role bindings for the static token named 'admin' + - kind: ClusterRole + name: cluster-admin + permissions: # here, additional permissions can be configured + - rules: + - apiGroups: [ '' ] + resources: [ 'secretcs'] + verbs: [ '*' ] + - name: viewer + permissions: + - rules: + - apiGroups: [ '' ] + resources: [ 'pods', 'services' ] + verbs: [ 'get', 'list', 'watch' ] + ``` ### Purpose Overriding diff --git a/internal/controllers/managedcontrolplane/access.go b/internal/controllers/managedcontrolplane/access.go index 5db8f9c9..8c456a2d 100644 --- a/internal/controllers/managedcontrolplane/access.go +++ b/internal/controllers/managedcontrolplane/access.go @@ -23,7 +23,7 @@ import ( corev2alpha1 "github.com/openmcp-project/openmcp-operator/api/core/v2alpha1" ) -// manageAccessRequests aligns the existing AccessRequests for the MCP with the currently configured OIDC providers. +// manageAccessRequests aligns the existing AccessRequests for the MCP with the currently configured OIDC providers and tokens. // It uses the given createCon function to create conditions for AccessRequests and returns a set of conditions that should be removed from the MCP status. // The bool return value specifies whether everything related to MCP access is in the desired state or not. If 'false', it is recommended to requeue the MCP. func (r *ManagedControlPlaneReconciler) manageAccessRequests(ctx context.Context, mcp *corev2alpha1.ManagedControlPlaneV2, platformNamespace string, cr *clustersv1alpha1.ClusterRequest, createCon func(conType string, status metav1.ConditionStatus, reason, message string)) (bool, sets.Set[string], errutils.ReasonableError) { @@ -50,7 +50,7 @@ func (r *ManagedControlPlaneReconciler) manageAccessRequests(ctx context.Context // remove conditions for AccessRequests that are neither required nor in deletion (= have been deleted already) // first, build a set of OIDC provider names that have a condition in the MCP status removeConditions := collections.AggregateSlice(mcp.Status.Conditions, func(con metav1.Condition, cur sets.Set[string]) sets.Set[string] { - if providerName, found := strings.CutPrefix(con.Type, corev2alpha1.ConditionPrefixOIDCAccessReady); found { + if providerName, found := strings.CutPrefix(con.Type, corev2alpha1.ConditionPrefixAccessReady); found { cur.Insert(providerName) } return cur @@ -61,7 +61,7 @@ func (r *ManagedControlPlaneReconciler) manageAccessRequests(ctx context.Context removeConditions = removeConditions.Difference(accessRequestsInDeletion) // now, add the condition prefix again removeConditions = collections.ProjectMapToMap(removeConditions, func(providerName string, _ sets.Empty) (string, sets.Empty) { - return corev2alpha1.ConditionPrefixOIDCAccessReady + providerName, sets.Empty{} + return corev2alpha1.ConditionPrefixAccessReady + providerName, sets.Empty{} }) everythingReady := accessRequestsInDeletion.Len() == 0 && accessSecretsInDeletion.Len() == 0 && allAccessReady @@ -78,31 +78,43 @@ func (r *ManagedControlPlaneReconciler) manageAccessRequests(ctx context.Context return everythingReady, removeConditions, nil } -// createOrUpdateDesiredAccessRequests creates/updates all AccessRequests that are desired according to the ManagedControlPlane's configured OIDC providers. +// createOrUpdateDesiredAccessRequests creates/updates all AccessRequests that are desired according to the ManagedControlPlane's configured OIDC providers and tokens. // It returns a mapping from OIDC provider names to the corresponding AccessRequests. // If the ManagedControlPlane has a non-zero DeletionTimestamp, no AccessRequests will be created or updated and the returned map will be empty. func (r *ManagedControlPlaneReconciler) createOrUpdateDesiredAccessRequests(ctx context.Context, mcp *corev2alpha1.ManagedControlPlaneV2, platformNamespace string, cr *clustersv1alpha1.ClusterRequest, createCon func(conType string, status metav1.ConditionStatus, reason, message string)) (map[string]*clustersv1alpha1.AccessRequest, errutils.ReasonableError) { log := logging.FromContextOrPanic(ctx) updatedAccessRequests := map[string]*clustersv1alpha1.AccessRequest{} - var oidcProviders []*commonapi.OIDCProviderConfig + var oidcProviders []commonapi.OIDCProviderConfig + var tokenProviders []corev2alpha1.TokenConfig // create or update AccessRequests for the ManagedControlPlane if mcp.DeletionTimestamp.IsZero() { - oidcProviders = make([]*commonapi.OIDCProviderConfig, 0, len(mcp.Spec.IAM.OIDCProviders)+1) - if r.Config.DefaultOIDCProvider != nil && len(mcp.Spec.IAM.RoleBindings) > 0 { + oidcProviders = make([]commonapi.OIDCProviderConfig, 0, len(mcp.Spec.IAM.OIDC.ExtraProviders)+1) + if r.Config.DefaultOIDCProvider != nil && len(mcp.Spec.IAM.OIDC.DefaultProvider.RoleBindings) > 0 { // add default OIDC provider, unless it has been disabled defaultOidc := r.Config.DefaultOIDCProvider.DeepCopy() defaultOidc.Name = corev2alpha1.DefaultOIDCProviderName - defaultOidc.RoleBindings = mcp.Spec.IAM.RoleBindings - oidcProviders = append(oidcProviders, defaultOidc) + defaultOidc.RoleBindings = mcp.Spec.IAM.OIDC.DefaultProvider.RoleBindings + oidcProviders = append(oidcProviders, *defaultOidc) } - oidcProviders = append(oidcProviders, mcp.Spec.IAM.OIDCProviders...) + oidcProviders = append(oidcProviders, mcp.Spec.IAM.OIDC.ExtraProviders...) + + tokenProviders = mcp.Spec.IAM.Tokens + } + + setArLabels := func(ar *clustersv1alpha1.AccessRequest) { + if ar.Labels == nil { + ar.Labels = map[string]string{} + } + ar.Labels[corev2alpha1.MCPNameLabel] = mcp.Name + ar.Labels[corev2alpha1.MCPNamespaceLabel] = mcp.Namespace + ar.Labels[apiconst.ManagedByLabel] = ControllerName } for _, oidc := range oidcProviders { log.Debug("Creating/updating AccessRequest for OIDC provider", "oidcProviderName", oidc.Name) - arName := ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name) + arName := ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+oidc.Name) ar := &clustersv1alpha1.AccessRequest{} ar.Name = arName ar.Namespace = platformNamespace @@ -112,26 +124,51 @@ func (r *ManagedControlPlaneReconciler) createOrUpdateDesiredAccessRequests(ctx Namespace: cr.Namespace, } ar.Spec.OIDC = &clustersv1alpha1.OIDCConfig{ - OIDCProviderConfig: *oidc, + OIDCProviderConfig: oidc, } // set labels - if ar.Labels == nil { - ar.Labels = map[string]string{} - } - ar.Labels[corev2alpha1.MCPNameLabel] = mcp.Name - ar.Labels[corev2alpha1.MCPNamespaceLabel] = mcp.Namespace - ar.Labels[apiconst.ManagedByLabel] = ControllerName + setArLabels(ar) ar.Labels[corev2alpha1.OIDCProviderLabel] = oidc.Name return nil }); err != nil { rerr := errutils.WithReason(fmt.Errorf("error creating/updating AccessRequest '%s/%s': %w", ar.Namespace, ar.Name, err), cconst.ReasonPlatformClusterInteractionProblem) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+oidc.Name, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + createCon(corev2alpha1.ConditionPrefixAccessReady+oidc.Name, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error creating/updating AccessRequest for OIDC provider "+oidc.Name) return nil, rerr } - updatedAccessRequests[oidc.Name] = ar + updatedAccessRequests[corev2alpha1.OIDCNamePrefix+oidc.Name] = ar + } + + for _, token := range tokenProviders { + log.Debug("Creating/updating AccessRequest for token", "tokenName", token.Name) + // add the "token:" prefix to avoid name clashes with OIDC providers + arName := ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.TokenNamePrefix+token.Name) + ar := &clustersv1alpha1.AccessRequest{} + ar.Name = arName + ar.Namespace = platformNamespace + if _, err := controllerutil.CreateOrUpdate(ctx, r.PlatformCluster.Client(), ar, func() error { + ar.Spec.RequestRef = &commonapi.ObjectReference{ + Name: cr.Name, + Namespace: cr.Namespace, + } + ar.Spec.Token = &clustersv1alpha1.TokenConfig{ + Permissions: token.Permissions, + RoleRefs: token.RoleRefs, + } + + setArLabels(ar) + ar.Labels[corev2alpha1.TokenProviderLabel] = token.Name + + return nil + }); err != nil { + rerr := errutils.WithReason(fmt.Errorf("error creating/updating AccessRequest '%s/%s': %w", ar.Namespace, ar.Name, err), cconst.ReasonPlatformClusterInteractionProblem) + createCon(corev2alpha1.ConditionPrefixAccessReady+corev2alpha1.TokenNamePrefix+token.Name, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error creating/updating AccessRequest for token "+token.Name) + return nil, rerr + } + updatedAccessRequests[corev2alpha1.TokenNamePrefix+token.Name] = ar } return updatedAccessRequests, nil @@ -145,39 +182,57 @@ func (r *ManagedControlPlaneReconciler) deleteUndesiredAccessRequests(ctx contex accessRequestsInDeletion := sets.New[string]() - // delete all AccessRequests that have previously been created for this ManagedControlPlane but are not needed anymore - oidcARs := &clustersv1alpha1.AccessRequestList{} - if err := r.PlatformCluster.Client().List(ctx, oidcARs, client.InNamespace(platformNamespace), client.HasLabels{corev2alpha1.OIDCProviderLabel}, client.MatchingLabels{ + matchingLabels := client.MatchingLabels{ corev2alpha1.MCPNameLabel: mcp.Name, corev2alpha1.MCPNamespaceLabel: mcp.Namespace, apiconst.ManagedByLabel: ControllerName, - }); err != nil { + } + + // delete all AccessRequests that have previously been created for this ManagedControlPlane but are not needed anymore + oidcARs := &clustersv1alpha1.AccessRequestList{} + tokenArs := &clustersv1alpha1.AccessRequestList{} + + if err := r.PlatformCluster.Client().List(ctx, oidcARs, client.InNamespace(platformNamespace), client.HasLabels{corev2alpha1.OIDCProviderLabel}, matchingLabels); err != nil { rerr := errutils.WithReason(fmt.Errorf("error listing AccessRequests for ManagedControlPlane '%s/%s': %w", mcp.Namespace, mcp.Name, err), cconst.ReasonPlatformClusterInteractionProblem) createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) return accessRequestsInDeletion, rerr } + + if err := r.PlatformCluster.Client().List(ctx, tokenArs, client.InNamespace(platformNamespace), client.HasLabels{corev2alpha1.TokenProviderLabel}, matchingLabels); err != nil { + rerr := errutils.WithReason(fmt.Errorf("error listing AccessRequests for ManagedControlPlane '%s/%s': %w", mcp.Namespace, mcp.Name, err), cconst.ReasonPlatformClusterInteractionProblem) + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + return accessRequestsInDeletion, rerr + } + + mcpARs := &clustersv1alpha1.AccessRequestList{} + mcpARs.Items = append(mcpARs.Items, oidcARs.Items...) + mcpARs.Items = append(mcpARs.Items, tokenArs.Items...) + errs := errutils.NewReasonableErrorList() - for _, ar := range oidcARs.Items { - if _, ok := updatedAccessRequests[ar.Spec.OIDC.Name]; ok { - continue - } + for _, ar := range mcpARs.Items { providerName := "" if ar.Spec.OIDC != nil { - providerName = ar.Spec.OIDC.Name + providerName = corev2alpha1.OIDCNamePrefix + ar.Spec.OIDC.Name + } + if ar.Spec.Token != nil { + providerName = corev2alpha1.TokenNamePrefix + ar.Labels[corev2alpha1.TokenProviderLabel] + } + if _, ok := updatedAccessRequests[providerName]; ok { + continue } accessRequestsInDeletion.Insert(providerName) if !ar.DeletionTimestamp.IsZero() { log.Debug("Waiting for deletion of AccessRequest that is no longer required", "accessRequestName", ar.Name, "accessRequestNamespace", ar.Namespace, "oidcProviderName", providerName) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "AccessRequest is being deleted") + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "AccessRequest is being deleted") continue } log.Debug("Deleting AccessRequest that is no longer needed", "accessRequestName", ar.Name, "accessRequestNamespace", ar.Namespace, "oidcProviderName", providerName) if err := r.PlatformCluster.Client().Delete(ctx, &ar); client.IgnoreNotFound(err) != nil { rerr := errutils.WithReason(fmt.Errorf("error deleting AccessRequest '%s/%s': %w", ar.Namespace, ar.Name, err), cconst.ReasonPlatformClusterInteractionProblem) errs.Append(rerr) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) } - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "AccessRequest is being deleted") + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "AccessRequest is being deleted") } if rerr := errs.Aggregate(); rerr != nil { createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "Error deleting AccessRequests that are no longer needed") @@ -195,41 +250,64 @@ func (r *ManagedControlPlaneReconciler) deleteUndesiredAccessSecrets(ctx context accessSecretsInDeletion := sets.New[string]() - // delete all AccessRequest secrets that have been copied to the Onboarding cluster and belong to AccessRequests that are no longer needed - mcpSecrets := &corev1.SecretList{} - if err := r.OnboardingCluster.Client().List(ctx, mcpSecrets, client.InNamespace(mcp.Namespace), client.HasLabels{corev2alpha1.OIDCProviderLabel}, client.MatchingLabels{ + matchingLabels := client.MatchingLabels{ corev2alpha1.MCPNameLabel: mcp.Name, corev2alpha1.MCPNamespaceLabel: mcp.Namespace, apiconst.ManagedByLabel: ControllerName, - }); err != nil { + } + + // delete all AccessRequest secrets that have been copied to the Onboarding cluster and belong to AccessRequests that are no longer needed + oidcSecrets := &corev1.SecretList{} + tokenSecrets := &corev1.SecretList{} + + if err := r.OnboardingCluster.Client().List(ctx, oidcSecrets, client.InNamespace(mcp.Namespace), client.HasLabels{corev2alpha1.OIDCProviderLabel}, matchingLabels); err != nil { + rerr := errutils.WithReason(fmt.Errorf("error listing secrets for ManagedControlPlane '%s/%s': %w", mcp.Namespace, mcp.Name, err), cconst.ReasonOnboardingClusterInteractionProblem) + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + return accessSecretsInDeletion, rerr + } + + if err := r.OnboardingCluster.Client().List(ctx, tokenSecrets, client.InNamespace(mcp.Namespace), client.HasLabels{corev2alpha1.TokenProviderLabel}, matchingLabels); err != nil { rerr := errutils.WithReason(fmt.Errorf("error listing secrets for ManagedControlPlane '%s/%s': %w", mcp.Namespace, mcp.Name, err), cconst.ReasonOnboardingClusterInteractionProblem) createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) return accessSecretsInDeletion, rerr } + mcpSecrets := &corev1.SecretList{} + mcpSecrets.Items = append(mcpSecrets.Items, oidcSecrets.Items...) + mcpSecrets.Items = append(mcpSecrets.Items, tokenSecrets.Items...) + errs := errutils.NewReasonableErrorList() for _, mcpSecret := range mcpSecrets.Items { - providerName := mcpSecret.Labels[corev2alpha1.OIDCProviderLabel] - if providerName == "" { - log.Error(nil, "Secret for ManagedControlPlane has an empty OIDCProvider label, this should not happen", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace) + oidcProviderName := mcpSecret.Labels[corev2alpha1.OIDCProviderLabel] + tokeProviderName := mcpSecret.Labels[corev2alpha1.TokenProviderLabel] + if oidcProviderName == "" && tokeProviderName == "" { + log.Error(nil, "Secret for ManagedControlPlane has an empty OIDCProvider and empty TokenProvider label, this should not happen", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace) + continue + } + if oidcProviderName != "" && tokeProviderName != "" { + log.Error(nil, "Secret for ManagedControlPlane has both OIDCProvider and TokenProvider label set, this should not happen", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace, "oidcProviderName", oidcProviderName, "tokenProviderName", tokeProviderName) continue } + providerName := corev2alpha1.OIDCNamePrefix + oidcProviderName + if tokeProviderName != "" { + providerName = corev2alpha1.TokenNamePrefix + tokeProviderName + } if _, ok := updatedAccessRequests[providerName]; ok { continue } accessSecretsInDeletion.Insert(providerName) if !mcpSecret.DeletionTimestamp.IsZero() { - log.Debug("Waiting for deletion of access secret that is no longer required", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace, "oidcProviderName", providerName) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "AccessRequest secret is being deleted") + log.Debug("Waiting for deletion of access secret that is no longer required", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace, "oidcProviderName", oidcProviderName) + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "AccessRequest secret is being deleted") continue } - log.Debug("Deleting access secret that is no longer required", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace, "oidcProviderName", providerName) + log.Debug("Deleting access secret that is no longer required", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace, "oidcProviderName", oidcProviderName) if err := r.OnboardingCluster.Client().Delete(ctx, &mcpSecret); client.IgnoreNotFound(err) != nil { rerr := errutils.WithReason(fmt.Errorf("error deleting access secret '%s/%s': %w", mcpSecret.Namespace, mcpSecret.Name, err), cconst.ReasonOnboardingClusterInteractionProblem) errs.Append(rerr) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) } - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "access secret is being deleted") + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "access secret is being deleted") } if rerr := errs.Aggregate(); rerr != nil { createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequestDeletion, "Error deleting access secrets that are no longer needed") @@ -256,7 +334,7 @@ func (r *ManagedControlPlaneReconciler) syncAccessSecrets(ctx context.Context, m for providerName, ar := range updatedAccessRequests { if !ar.Status.IsGranted() || ar.Status.SecretRef == nil { log.Debug("AccessRequest is not ready yet", "accessRequestName", ar.Name, "accessRequestNamespace", ar.Namespace, "oidcProviderName", providerName) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "AccessRequest is not ready yet") + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "AccessRequest is not ready yet") allAccessReady = false } else { // copy access request secret and reference it in the ManagedControlPlane status @@ -265,12 +343,12 @@ func (r *ManagedControlPlaneReconciler) syncAccessSecrets(ctx context.Context, m arSecret.Namespace = ar.Namespace if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(arSecret), arSecret); err != nil { rerr := errutils.WithReason(fmt.Errorf("error getting AccessRequest secret '%s/%s': %w", arSecret.Namespace, arSecret.Name, err), cconst.ReasonPlatformClusterInteractionProblem) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error getting AccessRequest secret for OIDC provider "+providerName) return false, rerr } mcpSecret := &corev1.Secret{} - mcpSecret.Name = ctrlutils.K8sNameUUIDUnsafe(mcp.Name, providerName) + mcpSecret.Name = ctrlutils.NameHashSHAKE128Base32(mcp.Name, providerName) mcpSecret.Namespace = mcp.Namespace if _, err := controllerutil.CreateOrUpdate(ctx, r.OnboardingCluster.Client(), mcpSecret, func() error { mcpSecret.Data = arSecret.Data @@ -279,7 +357,12 @@ func (r *ManagedControlPlaneReconciler) syncAccessSecrets(ctx context.Context, m } mcpSecret.Labels[corev2alpha1.MCPNameLabel] = mcp.Name mcpSecret.Labels[corev2alpha1.MCPNamespaceLabel] = mcp.Namespace - mcpSecret.Labels[corev2alpha1.OIDCProviderLabel] = providerName + if ar.Spec.OIDC != nil { + mcpSecret.Labels[corev2alpha1.OIDCProviderLabel] = ar.Spec.OIDC.Name + } + if ar.Spec.Token != nil { + mcpSecret.Labels[corev2alpha1.TokenProviderLabel] = ar.Labels[corev2alpha1.TokenProviderLabel] + } mcpSecret.Labels[apiconst.ManagedByLabel] = ControllerName if err := controllerutil.SetControllerReference(mcp, mcpSecret, r.OnboardingCluster.Scheme()); err != nil { @@ -288,7 +371,7 @@ func (r *ManagedControlPlaneReconciler) syncAccessSecrets(ctx context.Context, m return nil }); err != nil { rerr := errutils.WithReason(fmt.Errorf("error creating/updating AccessRequest secret '%s/%s': %w", mcpSecret.Namespace, mcpSecret.Name, err), cconst.ReasonOnboardingClusterInteractionProblem) - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error creating/updating AccessRequest secret for OIDC provider "+providerName) return false, rerr } @@ -296,7 +379,7 @@ func (r *ManagedControlPlaneReconciler) syncAccessSecrets(ctx context.Context, m mcp.Status.Access[providerName] = commonapi.LocalObjectReference{ Name: mcpSecret.Name, } - createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionTrue, "", "") + createCon(corev2alpha1.ConditionPrefixAccessReady+providerName, metav1.ConditionTrue, "", "") } } diff --git a/internal/controllers/managedcontrolplane/controller_test.go b/internal/controllers/managedcontrolplane/controller_test.go index c81bdd94..a91b9f2f 100644 --- a/internal/controllers/managedcontrolplane/controller_test.go +++ b/internal/controllers/managedcontrolplane/controller_test.go @@ -198,8 +198,8 @@ var _ = Describe("ManagedControlPlane Controller", func() { WithStatus(metav1.ConditionTrue)), )) oidcProviders := []commonapi.OIDCProviderConfig{*rec.Config.DefaultOIDCProvider.DeepCopy()} - oidcProviders[0].RoleBindings = mcp.Spec.IAM.RoleBindings - for _, addProv := range mcp.Spec.IAM.OIDCProviders { + oidcProviders[0].RoleBindings = mcp.Spec.IAM.OIDC.DefaultProvider.RoleBindings + for _, addProv := range mcp.Spec.IAM.OIDC.ExtraProviders { oidcProviders = append(oidcProviders, *addProv.DeepCopy()) } Expect(oidcProviders).To(HaveLen(3)) @@ -207,12 +207,12 @@ var _ = Describe("ManagedControlPlane Controller", func() { By("verifying that the AccessRequest is not ready for oidc provider: " + oidc.Name) Expect(mcp.Status.Conditions).To(ContainElements( MatchCondition(TestCondition(). - WithType(corev2alpha1.ConditionPrefixOIDCAccessReady + oidc.Name). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.OIDCNamePrefix + oidc.Name). WithStatus(metav1.ConditionFalse). WithReason(cconst.ReasonWaitingForAccessRequest)), )) ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name)) + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+oidc.Name)) ar.SetNamespace(platformNamespace) Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) Expect(ar.Spec.RequestRef.Name).To(Equal(cr.Name)) @@ -220,13 +220,31 @@ var _ = Describe("ManagedControlPlane Controller", func() { Expect(ar.Spec.OIDC).ToNot(BeNil()) Expect(ar.Spec.OIDC.OIDCProviderConfig).To(Equal(oidc)) } + for _, token := range mcp.Spec.IAM.Tokens { + By("verifying that the AccessRequest is not ready for token: " + token.Name) + Expect(mcp.Status.Conditions).To(ContainElements( + MatchCondition(TestCondition(). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.TokenNamePrefix + token.Name). + WithStatus(metav1.ConditionFalse). + WithReason(cconst.ReasonWaitingForAccessRequest)), + )) + ar := &clustersv1alpha1.AccessRequest{} + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.TokenNamePrefix+token.Name)) + ar.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) + Expect(ar.Spec.RequestRef.Name).To(Equal(cr.Name)) + Expect(ar.Spec.RequestRef.Namespace).To(Equal(cr.Namespace)) + Expect(ar.Spec.Token).ToNot(BeNil()) + Expect(ar.Spec.Token.Permissions).To(Equal(token.Permissions)) + Expect(ar.Spec.Token.RoleRefs).To(Equal(token.RoleRefs)) + } // fake AccessRequest ready status By("fake: AccessRequest readiness") for _, oidc := range oidcProviders { By("fake: AccessRequest readiness for oidc provider: " + oidc.Name) ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name)) + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+oidc.Name)) ar.SetNamespace(platformNamespace) Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) ar.Status.Phase = clustersv1alpha1.REQUEST_GRANTED @@ -238,7 +256,29 @@ var _ = Describe("ManagedControlPlane Controller", func() { sec.SetName(ar.Status.SecretRef.Name) sec.SetNamespace(ar.Namespace) sec.Data = map[string][]byte{ - clustersv1alpha1.SecretKeyKubeconfig: []byte(oidc.Name), + clustersv1alpha1.SecretKeyKubeconfig: []byte(corev2alpha1.OIDCNamePrefix + oidc.Name), + } + Expect(env.Client(platform).Status().Update(env.Ctx, ar)).To(Succeed()) + Expect(env.Client(platform).Create(env.Ctx, sec)).To(Succeed()) + } + + tokens := mcp.Spec.IAM.Tokens + for _, token := range tokens { + By("fake: Token AccessRequest readiness for token: " + token.Name) + ar := &clustersv1alpha1.AccessRequest{} + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.TokenNamePrefix+token.Name)) + ar.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) + ar.Status.Phase = clustersv1alpha1.REQUEST_GRANTED + ar.Status.SecretRef = &commonapi.ObjectReference{ + Name: ar.Name, + Namespace: ar.Namespace, + } + sec := &corev1.Secret{} + sec.SetName(ar.Status.SecretRef.Name) + sec.SetNamespace(ar.Namespace) + sec.Data = map[string][]byte{ + clustersv1alpha1.SecretKeyKubeconfig: []byte(corev2alpha1.TokenNamePrefix + token.Name), } Expect(env.Client(platform).Status().Update(env.Ctx, ar)).To(Succeed()) Expect(env.Client(platform).Create(env.Ctx, sec)).To(Succeed()) @@ -264,13 +304,20 @@ var _ = Describe("ManagedControlPlane Controller", func() { for _, oidc := range oidcProviders { Expect(mcp.Status.Conditions).To(ContainElements( MatchCondition(TestCondition(). - WithType(corev2alpha1.ConditionPrefixOIDCAccessReady + oidc.Name). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.OIDCNamePrefix + oidc.Name). WithStatus(metav1.ConditionTrue)), )) } - Expect(mcp.Status.Access).To(HaveLen(len(oidcProviders))) + for _, token := range tokens { + Expect(mcp.Status.Conditions).To(ContainElements( + MatchCondition(TestCondition(). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.TokenNamePrefix + token.Name). + WithStatus(metav1.ConditionTrue)), + )) + } + Expect(mcp.Status.Access).To(HaveLen(len(oidcProviders) + len(tokens))) for providerName, secretRef := range mcp.Status.Access { - By("verifying MCP access secret for oidc provider: " + providerName) + By("verifying MCP access secret for oidc provider/token: " + providerName) sec := &corev1.Secret{} sec.SetName(secretRef.Name) sec.SetNamespace(mcp.Namespace) @@ -280,23 +327,38 @@ var _ = Describe("ManagedControlPlane Controller", func() { By("=== UPDATE ===") - // change the rolebindings in the MCP spec and remove one OIDC provider + // change the rolebindings in the MCP spec and remove one OIDC provider and one token By("updating MCP spec") - mcp.Spec.IAM.RoleBindings = mcp.Spec.IAM.RoleBindings[:len(mcp.Spec.IAM.RoleBindings)-1] - removedOIDCProviderName := mcp.Spec.IAM.OIDCProviders[len(mcp.Spec.IAM.OIDCProviders)-1].Name - toBeRemovedSecretName := mcp.Status.Access[removedOIDCProviderName].Name - mcp.Spec.IAM.OIDCProviders = mcp.Spec.IAM.OIDCProviders[:len(mcp.Spec.IAM.OIDCProviders)-1] + // oidc + mcp.Spec.IAM.OIDC.DefaultProvider.RoleBindings = mcp.Spec.IAM.OIDC.DefaultProvider.RoleBindings[:len(mcp.Spec.IAM.OIDC.DefaultProvider.RoleBindings)-1] + removedOIDCProviderName := mcp.Spec.IAM.OIDC.ExtraProviders[len(mcp.Spec.IAM.OIDC.ExtraProviders)-1].Name + toBeRemovedSecretNameOIDC := mcp.Status.Access[removedOIDCProviderName].Name + mcp.Spec.IAM.OIDC.ExtraProviders = mcp.Spec.IAM.OIDC.ExtraProviders[:len(mcp.Spec.IAM.OIDC.ExtraProviders)-1] + // token + mcp.Spec.IAM.Tokens = mcp.Spec.IAM.Tokens[:len(mcp.Spec.IAM.Tokens)-1] + removedTokenName := tokens[len(tokens)-1].Name + toBeRemovedSecretNameToken := mcp.Status.Access[removedTokenName].Name Expect(env.Client(onboarding).Update(env.Ctx, mcp)).To(Succeed()) By("fake: adding finalizers to AccessRequests") for _, oidc := range oidcProviders { By("fake: adding finalizer to AccessRequest for oidc provider: " + oidc.Name) ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name)) + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+oidc.Name)) + ar.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) + controllerutil.AddFinalizer(ar, "dummy") + Expect(env.Client(platform).Update(env.Ctx, ar)).To(Succeed()) + } + for _, token := range tokens { + By("fake: adding finalizer to AccessRequest for token: " + token.Name) + ar := &clustersv1alpha1.AccessRequest{} + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.TokenNamePrefix+token.Name)) ar.SetNamespace(platformNamespace) Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) controllerutil.AddFinalizer(ar, "dummy") Expect(env.Client(platform).Update(env.Ctx, ar)).To(Succeed()) + } // reconcile the MCP @@ -327,7 +389,7 @@ var _ = Describe("ManagedControlPlane Controller", func() { removedOIDCIdx = i Expect(mcp.Status.Conditions).To(ContainElements( MatchCondition(TestCondition(). - WithType(corev2alpha1.ConditionPrefixOIDCAccessReady + oidc.Name). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.OIDCNamePrefix + oidc.Name). WithStatus(metav1.ConditionFalse). WithReason(cconst.ReasonWaitingForAccessRequestDeletion), ))) @@ -335,34 +397,76 @@ var _ = Describe("ManagedControlPlane Controller", func() { } else { Expect(mcp.Status.Conditions).To(ContainElements( MatchCondition(TestCondition(). - WithType(corev2alpha1.ConditionPrefixOIDCAccessReady + oidc.Name). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.OIDCNamePrefix + oidc.Name). WithStatus(metav1.ConditionTrue), ))) - Expect(mcp.Status.Access).To(HaveKey(oidc.Name)) + Expect(mcp.Status.Access).To(HaveKey(corev2alpha1.OIDCNamePrefix + oidc.Name)) } } Expect(removedOIDCIdx).To(BeNumerically(">", -1)) oidcProviders = append(oidcProviders[:removedOIDCIdx], oidcProviders[removedOIDCIdx+1:]...) - Expect(mcp.Status.Access).ToNot(HaveKey(removedOIDCProviderName)) - sec := &corev1.Secret{} - sec.SetName(toBeRemovedSecretName) - sec.SetNamespace(mcp.Namespace) - Expect(env.Client(onboarding).Get(env.Ctx, client.ObjectKeyFromObject(sec), sec)).To(MatchError(apierrors.IsNotFound, "IsNotFound")) - ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, removedOIDCProviderName)) - ar.SetNamespace(platformNamespace) - Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) - Expect(ar.GetDeletionTimestamp().IsZero()).To(BeFalse()) + Expect(mcp.Status.Access).ToNot(HaveKey(corev2alpha1.OIDCNamePrefix + removedOIDCProviderName)) + + removedTokenIdx := -1 + for i, token := range tokens { + By("verifying condition for token: " + token.Name) + if token.Name == removedTokenName { + removedTokenIdx = i + Expect(mcp.Status.Conditions).To(ContainElements( + MatchCondition(TestCondition(). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.TokenNamePrefix + token.Name). + WithStatus(metav1.ConditionFalse). + WithReason(cconst.ReasonWaitingForAccessRequestDeletion), + ))) + Expect(mcp.Status.Access).ToNot(HaveKey(token.Name)) + } else { + Expect(mcp.Status.Conditions).To(ContainElements( + MatchCondition(TestCondition(). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.TokenNamePrefix + token.Name). + WithStatus(metav1.ConditionTrue), + ))) + Expect(mcp.Status.Access).To(HaveKey(corev2alpha1.TokenNamePrefix + token.Name)) + } + } + Expect(removedTokenIdx).To(BeNumerically(">", -1)) + tokens = append(tokens[:removedTokenIdx], tokens[removedTokenIdx+1:]...) + Expect(mcp.Status.Access).ToNot(HaveKey(corev2alpha1.TokenNamePrefix + removedTokenName)) + + secOIDC := &corev1.Secret{} + secOIDC.SetName(toBeRemovedSecretNameOIDC) + secOIDC.SetNamespace(mcp.Namespace) + Expect(env.Client(onboarding).Get(env.Ctx, client.ObjectKeyFromObject(secOIDC), secOIDC)).To(MatchError(apierrors.IsNotFound, "IsNotFound")) + arOIDC := &clustersv1alpha1.AccessRequest{} + arOIDC.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+removedOIDCProviderName)) + arOIDC.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(arOIDC), arOIDC)).To(Succeed()) + Expect(arOIDC.GetDeletionTimestamp().IsZero()).To(BeFalse()) + + secToken := &corev1.Secret{} + secToken.SetName(toBeRemovedSecretNameToken) + secToken.SetNamespace(mcp.Namespace) + Expect(env.Client(onboarding).Get(env.Ctx, client.ObjectKeyFromObject(secToken), secToken)).To(MatchError(apierrors.IsNotFound, "IsNotFound")) + arToken := &clustersv1alpha1.AccessRequest{} + arToken.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.TokenNamePrefix+removedTokenName)) + arToken.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(arToken), arToken)).To(Succeed()) + Expect(arToken.GetDeletionTimestamp().IsZero()).To(BeFalse()) // remove dummy finalizer from AccessRequest belonging to the removed OIDC provider By("fake: removing dummy finalizer from AccessRequest for removed OIDC provider: " + removedOIDCProviderName) - controllerutil.RemoveFinalizer(ar, "dummy") - Expect(env.Client(platform).Update(env.Ctx, ar)).To(Succeed()) + controllerutil.RemoveFinalizer(arOIDC, "dummy") + Expect(env.Client(platform).Update(env.Ctx, arOIDC)).To(Succeed()) + // remove dummy finalizer from AccessRequest belonging to the removed token + By("fake: removing dummy finalizer from AccessRequest for removed token: " + removedTokenName) + controllerutil.RemoveFinalizer(arToken, "dummy") + Expect(env.Client(platform).Update(env.Ctx, arToken)).To(Succeed()) // reconcile the MCP again // expected outcome: // - the AccessRequest for the removed OIDC provider has been deleted + // - the AccessRequest for the removed token has been deleted // - the condition for the removed OIDC provider is gone + // - the condition for the removed token is gone // - the mcp should be requeued with a requeueAfter duration that matches the reconcile interval from the controller config By("second MCP reconciliation after update") res = env.ShouldReconcile(mcpRec, testutils.RequestFromObject(mcp)) @@ -380,14 +484,28 @@ var _ = Describe("ManagedControlPlane Controller", func() { By("verifying condition for oidc provider: " + oidc.Name) Expect(mcp.Status.Conditions).To(ContainElements( MatchCondition(TestCondition(). - WithType(corev2alpha1.ConditionPrefixOIDCAccessReady + oidc.Name). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.OIDCNamePrefix + oidc.Name). + WithStatus(metav1.ConditionTrue)), + )) + } + for _, token := range tokens { + By("verifying condition for token: " + token.Name) + Expect(mcp.Status.Conditions).To(ContainElements( + MatchCondition(TestCondition(). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.TokenNamePrefix + token.Name). WithStatus(metav1.ConditionTrue)), )) } Expect(mcp.Status.Access).ToNot(HaveKey(removedOIDCProviderName)) Expect(mcp.Status.Conditions).ToNot(ContainElements( MatchCondition(TestCondition(). - WithType(corev2alpha1.ConditionPrefixOIDCAccessReady + removedOIDCProviderName), + WithType(corev2alpha1.ConditionPrefixAccessReady + removedOIDCProviderName), + ), + )) + Expect(mcp.Status.Access).ToNot(HaveKey(removedTokenName)) + Expect(mcp.Status.Conditions).ToNot(ContainElements( + MatchCondition(TestCondition(). + WithType(corev2alpha1.ConditionPrefixAccessReady + removedTokenName), ), )) @@ -447,7 +565,15 @@ var _ = Describe("ManagedControlPlane Controller", func() { for _, oidc := range oidcProviders { By("verifying AccessRequest does not have a deletion timestamp for oidc provider: " + oidc.Name) ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name)) + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+oidc.Name)) + ar.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) + Expect(ar.DeletionTimestamp.IsZero()).To(BeTrue()) + } + for _, token := range tokens { + By("verifying AccessRequest does not have a deletion timestamp for token: " + token.Name) + ar := &clustersv1alpha1.AccessRequest{} + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.TokenNamePrefix+token.Name)) ar.SetNamespace(platformNamespace) Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) Expect(ar.DeletionTimestamp.IsZero()).To(BeTrue()) @@ -499,13 +625,13 @@ var _ = Describe("ManagedControlPlane Controller", func() { for _, oidc := range oidcProviders { By("verifying AccessRequest and access secret deletion status for oidc provider: " + oidc.Name) ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name)) + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+oidc.Name)) ar.SetNamespace(platformNamespace) Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) Expect(ar.DeletionTimestamp.IsZero()).To(BeFalse()) Expect(mcp.Status.Conditions).To(ContainElements( MatchCondition(TestCondition(). - WithType(corev2alpha1.ConditionPrefixOIDCAccessReady + oidc.Name). + WithType(corev2alpha1.ConditionPrefixAccessReady + corev2alpha1.OIDCNamePrefix + oidc.Name). WithStatus(metav1.ConditionFalse). WithReason(cconst.ReasonWaitingForAccessRequestDeletion), ), @@ -526,7 +652,16 @@ var _ = Describe("ManagedControlPlane Controller", func() { for _, oidc := range oidcProviders { By("fake: removing finalizer from AccessRequest for oidc provider: " + oidc.Name) ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name)) + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.OIDCNamePrefix+oidc.Name)) + ar.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) + controllerutil.RemoveFinalizer(ar, "dummy") + Expect(env.Client(platform).Update(env.Ctx, ar)).To(Succeed()) + } + for _, token := range tokens { + By("fake: removing finalizer from AccessRequest for token: " + token.Name) + ar := &clustersv1alpha1.AccessRequest{} + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, corev2alpha1.TokenNamePrefix+token.Name)) ar.SetNamespace(platformNamespace) Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed()) controllerutil.RemoveFinalizer(ar, "dummy") @@ -555,7 +690,14 @@ var _ = Describe("ManagedControlPlane Controller", func() { for _, oidc := range oidcProviders { By("verifying AccessRequest deletion for oidc provider: " + oidc.Name) ar := &clustersv1alpha1.AccessRequest{} - ar.SetName(ctrlutils.K8sNameUUIDUnsafe(mcp.Name, oidc.Name)) + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, oidc.Name)) + ar.SetNamespace(platformNamespace) + Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(MatchError(apierrors.IsNotFound, "IsNotFound")) + } + for _, token := range tokens { + By("verifying AccessRequest deletion for token: " + token.Name) + ar := &clustersv1alpha1.AccessRequest{} + ar.SetName(ctrlutils.NameHashSHAKE128Base32(mcp.Name, token.Name)) ar.SetNamespace(platformNamespace) Expect(env.Client(platform).Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(MatchError(apierrors.IsNotFound, "IsNotFound")) } diff --git a/internal/controllers/managedcontrolplane/testdata/test-01/onboarding/mcp-01.yaml b/internal/controllers/managedcontrolplane/testdata/test-01/onboarding/mcp-01.yaml index 688ba153..2a197eeb 100644 --- a/internal/controllers/managedcontrolplane/testdata/test-01/onboarding/mcp-01.yaml +++ b/internal/controllers/managedcontrolplane/testdata/test-01/onboarding/mcp-01.yaml @@ -8,42 +8,62 @@ metadata: - services.openmcp.cloud/sp-02 spec: iam: - roleBindings: - - subjects: - - kind: User - name: user1 - - kind: User - name: user2 - roleRefs: - - kind: ClusterRole - name: cluster-admin - - subjects: - - kind: Group - name: group1 - roleRefs: - - kind: ClusterRole - name: cluster-viewer - - oidcProviders: - - name: add1 - issuer: https://example.com/add1 - groupsPrefix: 'add1groups:' - usernamePrefix: 'add1user:' - roleBindings: - - subjects: - - kind: User - name: user3 - roleRefs: - - kind: ClusterRole - name: cluster-admin - - name: add2 - issuer: https://example.com/add2 - groupsPrefix: 'add2groups:' - usernamePrefix: 'add2user:' - roleBindings: - - subjects: - - kind: Group - name: group2 + tokens: + - name: admin roleRefs: - - kind: ClusterRole - name: cluster-viewer + - kind: ClusterRole + name: cluster-admin + permissions: + - rules: + - apiGroups: [ '' ] + resources: [ 'secretcs'] + verbs: [ '*' ] + - name: viewer + permissions: + - rules: + - apiGroups: [ '' ] + resources: [ 'pods', 'services' ] + verbs: [ 'get', 'list', 'watch' ] + + oidc: + defaultProvider: + roleBindings: + - subjects: + - kind: User + name: user1 + - kind: User + name: user2 + roleRefs: + - kind: ClusterRole + name: cluster-admin + - subjects: + - kind: Group + name: group1 + roleRefs: + - kind: ClusterRole + name: cluster-viewer + + extraProviders: + - name: add1 + issuer: https://example.com/add1 + groupsPrefix: 'add1groups:' + usernamePrefix: 'add1user:' + roleBindings: + - subjects: + - kind: User + name: user3 + roleRefs: + - kind: ClusterRole + name: cluster-admin + - name: add2 + issuer: https://example.com/add2 + groupsPrefix: 'add2groups:' + usernamePrefix: 'add2user:' + roleBindings: + - subjects: + - kind: Group + name: group2 + roleRefs: + - kind: ClusterRole + name: cluster-viewer +