diff --git a/VERSION b/VERSION index a5fb140..3061e9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.11.2-dev \ No newline at end of file +v0.12.0 \ No newline at end of file diff --git a/api/clusters/v1alpha1/accessrequest_types.go b/api/clusters/v1alpha1/accessrequest_types.go index bb97ce2..4837a56 100644 --- a/api/clusters/v1alpha1/accessrequest_types.go +++ b/api/clusters/v1alpha1/accessrequest_types.go @@ -16,6 +16,7 @@ const ( // +kubebuilder:validation:XValidation:rule="!has(oldSelf.clusterRef) || has(self.clusterRef)", message="clusterRef may not be removed once set" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.requestRef) || has(self.requestRef)", message="requestRef may not be removed once set" +// +kubebuilder:validation:XValidation:rule="(has(self.token) && !has(self.oidc)) || (!has(self.token) && has(self.oidc))",message="exactly one of spec.token or spec.oidc must be set" type AccessRequestSpec struct { // ClusterRef is the reference to the Cluster for which access is requested. // If set, requestRef will be ignored. @@ -31,21 +32,37 @@ type AccessRequestSpec struct { // +optional RequestRef *commonapi.ObjectReference `json:"requestRef,omitempty"` + // Token is the configuration for token-based access. + // Exactly one of Token or OIDC must be set. + // +optional + Token *TokenConfig `json:"token,omitempty"` + + // OIDC is the configuration for OIDC-based access. + // Exactly one of Token or OIDC must be set. + // +optional + OIDC *OIDCConfig `json:"oidc,omitempty"` +} + +type TokenConfig struct { // Permissions are the requested permissions. - // If not empty, corresponding Roles and ClusterRoles will be created in the target cluster, potentially also creating namespaces for Roles. - // For token-based access, the serviceaccount will be bound to the created Roles and ClusterRoles. + // 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. // +optional Permissions []PermissionsRequest `json:"permissions,omitempty"` - // RoleRefs are references to existing (Cluster)Roles that should be bound to the created serviceaccount or OIDC user. + // RoleRefs are references to existing (Cluster)Roles that should be bound to the created serviceaccount. // +optional RoleRefs []commonapi.RoleRef `json:"roleRefs,omitempty"` +} + +type OIDCConfig struct { + commonapi.OIDCProviderConfig `json:",inline"` - // OIDCProvider is a configuration for an OIDC provider that should be used for authentication and associated role bindings. - // If set, the handling ClusterProvider will create an OIDC-based access for the AccessRequest, if supported. - // Otherwise, a serviceaccount with a token will be created and bound to the requested permissions. + // Roles are additional (Cluster)Roles that should be created. + // Note that they are not automatically bound to any user. + // It is strongly recommended to set the name field so that the created (Cluster)Roles can be referenced in the RoleBindings field. // +optional - OIDCProvider *commonapi.OIDCProviderConfig `json:"oidcProvider,omitempty"` + Roles []PermissionsRequest `json:"roles,omitempty"` } type PermissionsRequest struct { diff --git a/api/clusters/v1alpha1/zz_generated.deepcopy.go b/api/clusters/v1alpha1/zz_generated.deepcopy.go index 18a688c..1c33e52 100644 --- a/api/clusters/v1alpha1/zz_generated.deepcopy.go +++ b/api/clusters/v1alpha1/zz_generated.deepcopy.go @@ -83,21 +83,14 @@ func (in *AccessRequestSpec) DeepCopyInto(out *AccessRequestSpec) { *out = new(common.ObjectReference) **out = **in } - if in.Permissions != nil { - in, out := &in.Permissions, &out.Permissions - *out = make([]PermissionsRequest, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.RoleRefs != nil { - in, out := &in.RoleRefs, &out.RoleRefs - *out = make([]common.RoleRef, len(*in)) - copy(*out, *in) + if in.Token != nil { + in, out := &in.Token, &out.Token + *out = new(TokenConfig) + (*in).DeepCopyInto(*out) } - if in.OIDCProvider != nil { - in, out := &in.OIDCProvider, &out.OIDCProvider - *out = new(common.OIDCProviderConfig) + if in.OIDC != nil { + in, out := &in.OIDC, &out.OIDC + *out = new(OIDCConfig) (*in).DeepCopyInto(*out) } } @@ -434,6 +427,29 @@ func (in *K8sConfiguration) DeepCopy() *K8sConfiguration { 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.OIDCProviderConfig.DeepCopyInto(&out.OIDCProviderConfig) + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]PermissionsRequest, 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 *PermissionsRequest) DeepCopyInto(out *PermissionsRequest) { *out = *in @@ -470,3 +486,30 @@ func (in *SupportedK8sVersion) DeepCopy() *SupportedK8sVersion { 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 + if in.Permissions != nil { + in, out := &in.Permissions, &out.Permissions + *out = make([]PermissionsRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RoleRefs != nil { + in, out := &in.RoleRefs, &out.RoleRefs + *out = make([]common.RoleRef, len(*in)) + copy(*out, *in) + } +} + +// 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/clusters.openmcp.cloud_accessrequests.yaml b/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml index ab619f5..5cecd5d 100644 --- a/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml +++ b/api/crds/manifests/clusters.openmcp.cloud_accessrequests.yaml @@ -66,11 +66,10 @@ spec: x-kubernetes-validations: - message: clusterRef is immutable rule: self == oldSelf - oidcProvider: + oidc: description: |- - OIDCProvider is a configuration for an OIDC provider that should be used for authentication and associated role bindings. - If set, the handling ClusterProvider will create an OIDC-based access for the AccessRequest, if supported. - Otherwise, a serviceaccount with a token will be created and bound to the requested permissions. + OIDC is the configuration for OIDC-based access. + Exactly one of Token or OIDC must be set. properties: clientID: description: ClientID is the client ID to use for the OIDC provider. @@ -181,6 +180,80 @@ spec: - subjects type: object type: array + roles: + description: |- + Roles are additional (Cluster)Roles that should be created. + Note that they are not automatically bound to any user. + It is strongly recommended to set the name field so that the created (Cluster)Roles can be referenced in the RoleBindings field. + items: + properties: + name: + description: |- + 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: |- + 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: |- + 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: + apiGroups: + description: |- + 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: |- + 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: + - verbs + type: object + type: array + required: + - rules + type: object + type: array usernameClaim: default: sub description: |- @@ -202,80 +275,6 @@ spec: - roleBindings - usernamePrefix type: object - permissions: - description: |- - Permissions are the requested permissions. - If not empty, corresponding Roles and ClusterRoles will be created in the target cluster, potentially also creating namespaces for Roles. - For token-based access, the serviceaccount will be bound to the created Roles and ClusterRoles. - items: - properties: - name: - description: |- - 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: |- - 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: |- - 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: - apiGroups: - description: |- - 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: |- - 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: - - verbs - type: object - type: array - required: - - rules - type: object - type: array requestRef: description: |- RequestRef is the reference to the ClusterRequest for whose Cluster access is requested. @@ -295,42 +294,124 @@ spec: x-kubernetes-validations: - message: requestRef is immutable rule: self == oldSelf - roleRefs: - description: RoleRefs are references to existing (Cluster)Roles that - should be bound to the created serviceaccount or OIDC user. - 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 + token: + description: |- + Token is the configuration for token-based access. + Exactly one of Token or OIDC must be set. + properties: + permissions: + description: |- + 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: + name: + description: |- + 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: |- + 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: |- + 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: + apiGroups: + description: |- + 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: |- + 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: + - verbs + type: object + type: array + required: + - rules + type: object + type: array + roleRefs: + 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. + 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 + type: object type: object x-kubernetes-validations: - message: clusterRef may not be removed once set rule: '!has(oldSelf.clusterRef) || has(self.clusterRef)' - message: requestRef may not be removed once set rule: '!has(oldSelf.requestRef) || has(self.requestRef)' + - message: exactly one of spec.token or spec.oidc must be set + rule: (has(self.token) && !has(self.oidc)) || (!has(self.token) && has(self.oidc)) status: description: AccessRequestStatus defines the observed state of AccessRequest properties: diff --git a/go.mod b/go.mod index 56ab3b3..60ab79e 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/onsi/ginkgo/v2 v2.25.2 github.com/onsi/gomega v1.38.2 github.com/openmcp-project/controller-utils v0.19.0 - github.com/openmcp-project/openmcp-operator/api v0.11.2 + github.com/openmcp-project/openmcp-operator/api v0.12.0 github.com/spf13/cobra v1.9.1 k8s.io/api v0.34.0 k8s.io/apimachinery v0.34.0 diff --git a/lib/clusteraccess/clusteraccess.go b/lib/clusteraccess/clusteraccess.go index d60efb4..52db0e2 100644 --- a/lib/clusteraccess/clusteraccess.go +++ b/lib/clusteraccess/clusteraccess.go @@ -431,7 +431,7 @@ func (m *managerImpl) CreateAndWaitForCluster(ctx context.Context, localName, pu Name: cr.Name, Namespace: cr.Namespace, }) - accessRequestMutator.WithPermissions(permissions) + accessRequestMutator.WithTokenPermissions(permissions) accessRequestMutator.WithMetadata(resources.NewMetadataMutator().WithLabels(map[string]string{ constv1alpha1.ManagedByLabel: m.controllerName, })) @@ -511,8 +511,8 @@ func ensureAccessRequest(ctx context.Context, platformClusterClient client.Clien permissions []clustersv1alpha1.PermissionsRequest, roleRefs []commonapi.RoleRef, metadata resources.MetadataMutator) (*clustersv1alpha1.AccessRequest, error) { mutator := newAccessRequestMutator(requestName, requestNamespace). - WithPermissions(permissions). - WithRoleRefs(roleRefs). + WithTokenPermissions(permissions). + WithTokenRoleRefs(roleRefs). WithMetadata(metadata) if requestRef != nil { @@ -677,13 +677,15 @@ func (m *clusterRequestMutator) Mutate(clusterRequest *clustersv1alpha1.ClusterR } type accessRequestMutator struct { - name string - namespace string - requestRef *commonapi.ObjectReference - clusterRef *commonapi.ObjectReference - permissions []clustersv1alpha1.PermissionsRequest - roleRefs []commonapi.RoleRef - metadata resources.MetadataMutator + name string + namespace string + requestRef *commonapi.ObjectReference + clusterRef *commonapi.ObjectReference + tokenPermissions []clustersv1alpha1.PermissionsRequest + tokenRoleRefs []commonapi.RoleRef + oidcProvider *commonapi.OIDCProviderConfig + oidcAdditionalRoles []clustersv1alpha1.PermissionsRequest + metadata resources.MetadataMutator } func newAccessRequestMutator(name, namespace string) *accessRequestMutator { @@ -703,13 +705,23 @@ func (m *accessRequestMutator) WithClusterRef(clusterRef *commonapi.ObjectRefere return m } -func (m *accessRequestMutator) WithPermissions(permissions []clustersv1alpha1.PermissionsRequest) *accessRequestMutator { - m.permissions = permissions +func (m *accessRequestMutator) WithTokenPermissions(permissions []clustersv1alpha1.PermissionsRequest) *accessRequestMutator { + m.tokenPermissions = permissions return m } -func (m *accessRequestMutator) WithRoleRefs(roleRefs []commonapi.RoleRef) *accessRequestMutator { - m.roleRefs = roleRefs +func (m *accessRequestMutator) WithTokenRoleRefs(roleRefs []commonapi.RoleRef) *accessRequestMutator { + m.tokenRoleRefs = roleRefs + return m +} + +func (m *accessRequestMutator) WithOIDCProvider(oidcProvider *commonapi.OIDCProviderConfig) *accessRequestMutator { + m.oidcProvider = oidcProvider + return m +} + +func (m *accessRequestMutator) WithOIDCAdditionalRoles(permissions []clustersv1alpha1.PermissionsRequest) *accessRequestMutator { + m.oidcAdditionalRoles = permissions return m } @@ -751,16 +763,41 @@ func (m *accessRequestMutator) MetadataMutator() resources.MetadataMutator { } func (m *accessRequestMutator) Mutate(accessRequest *clustersv1alpha1.AccessRequest) error { - accessRequest.Spec.Permissions = m.permissions + if m.tokenPermissions != nil { + if accessRequest.Spec.Token == nil { + accessRequest.Spec.Token = &clustersv1alpha1.TokenConfig{} + } + accessRequest.Spec.Token.Permissions = make([]clustersv1alpha1.PermissionsRequest, len(m.tokenPermissions)) + copy(accessRequest.Spec.Token.Permissions, m.tokenPermissions) + } + if m.tokenRoleRefs != nil { + if accessRequest.Spec.Token == nil { + accessRequest.Spec.Token = &clustersv1alpha1.TokenConfig{} + } + accessRequest.Spec.Token.RoleRefs = make([]commonapi.RoleRef, len(m.tokenRoleRefs)) + copy(accessRequest.Spec.Token.RoleRefs, m.tokenRoleRefs) + } - accessRequest.Spec.RoleRefs = m.roleRefs + if m.oidcProvider != nil { + if accessRequest.Spec.OIDC == nil { + accessRequest.Spec.OIDC = &clustersv1alpha1.OIDCConfig{} + } + accessRequest.Spec.OIDC.OIDCProviderConfig = *m.oidcProvider.DeepCopy() + } + if m.oidcAdditionalRoles != nil { + if accessRequest.Spec.OIDC == nil { + accessRequest.Spec.OIDC = &clustersv1alpha1.OIDCConfig{} + } + accessRequest.Spec.OIDC.Roles = make([]clustersv1alpha1.PermissionsRequest, len(m.oidcAdditionalRoles)) + copy(accessRequest.Spec.OIDC.Roles, m.oidcAdditionalRoles) + } if m.requestRef != nil { - accessRequest.Spec.RequestRef = m.requestRef + accessRequest.Spec.RequestRef = m.requestRef.DeepCopy() } if m.clusterRef != nil { - accessRequest.Spec.ClusterRef = m.clusterRef + accessRequest.Spec.ClusterRef = m.clusterRef.DeepCopy() } if m.metadata != nil { diff --git a/lib/go.mod b/lib/go.mod index 754bfd7..468bbba 100644 --- a/lib/go.mod +++ b/lib/go.mod @@ -8,7 +8,7 @@ require ( github.com/onsi/ginkgo/v2 v2.25.2 github.com/onsi/gomega v1.38.2 github.com/openmcp-project/controller-utils v0.19.0 - github.com/openmcp-project/openmcp-operator/api v0.11.2 + github.com/openmcp-project/openmcp-operator/api v0.12.0 k8s.io/api v0.34.0 k8s.io/apimachinery v0.34.0 k8s.io/client-go v0.34.0