diff --git a/packages/aws-cdk-lib/aws-eks/README.md b/packages/aws-cdk-lib/aws-eks/README.md index a32f94c651bb4..d6cef2fc604a4 100644 --- a/packages/aws-cdk-lib/aws-eks/README.md +++ b/packages/aws-cdk-lib/aws-eks/README.md @@ -1214,14 +1214,64 @@ cluster.grantAccess('eksAdminViewRoleAccess', eksAdminViewRole.roleArn, [ namespaces: ['foo', 'bar'], }), ]); + +// Custom permissions in EKS via group mapping +cluster.grantAccessToGroups('customAccess', 'arn:aws:iam::123456789012:role/testrole', ['custom-access-group']); + +// Now we can define permissions for the mapping +this.addManifest('customAccessPermissions', [ + { + apiVersion: 'rbac.authorization.k8s.io/v1', + kind: 'ClusterRole', + metadata: { + name: 'custom-accesss-role', + }, + rules: [ + { + apiGroups: [''], + resources: ['pods'], + verbs: ['get', 'list', 'watch'], + }, + { + apiGroups: ['apps'], + resources: ['deployments'], + verbs: ['create', 'update', 'delete'], + }, + { + apiGroups: ['rbac.authorization.k8s.io'], + resources: ['roles', 'rolebindings'], + verbs: ['create', 'bind', 'delete'], + }, + ], + }, + { + apiVersion: 'rbac.authorization.k8s.io/v1', + kind: 'ClusterRoleBinding', + metadata: { + name: 'custom-access-rolebinding', + }, + subjects: [ + { + kind: 'Group', + name: 'custom-access-group', + apiGroup: 'rbac.authorization.k8s.io', + }, + ], + roleRef: { + kind: 'ClusterRole', + name: 'custom-access-role', + apiGroup: 'rbac.authorization.k8s.io', + }, + }, +]); ``` ### Migrating from ConfigMap to Access Entry If the cluster is created with the `authenticationMode` property left undefined, -it will default to `CONFIG_MAP`. +it will default to `CONFIG_MAP`. -The update path is: +The update path is: `undefined`(`CONFIG_MAP`) -> `API_AND_CONFIG_MAP` -> `API` diff --git a/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts b/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts index 30f20ed0dee00..da31490d18f61 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts @@ -258,10 +258,27 @@ export enum AccessEntryType { EC2_WINDOWS = 'EC2_WINDOWS', } +/** + * Represents the Properties that can be granted + */ +export interface AccessEntryGrants { + /** + * The access policies that define the permissions and scope for the access entry. + * @default - No access policies are provided + */ + readonly accessPolicies?: IAccessPolicy[]; + /** + * The kubernetes groups you want to associate with this access policy. + * Those groups can be used as subjects in (Cluster)RoleBindings. + * @default - No kubernetes groups are provided + */ + readonly kubernetesGroups?: string[]; +} + /** * Represents the properties required to create an Amazon EKS access entry. */ -export interface AccessEntryProps { +export interface AccessEntryProps extends AccessEntryGrants { /** * The name of the AccessEntry. * @@ -278,10 +295,6 @@ export interface AccessEntryProps { * The Amazon EKS cluster to which the access entry applies. */ readonly cluster: ICluster; - /** - * The access policies that define the permissions and scope for the access entry. - */ - readonly accessPolicies: IAccessPolicy[]; /** * The Amazon Resource Name (ARN) of the principal (user or role) to associate the access entry with. */ @@ -323,28 +336,45 @@ export class AccessEntry extends Resource implements IAccessEntry { private cluster: ICluster; private principal: string; private accessPolicies: IAccessPolicy[]; + private kubernetesGroups: string[]; constructor(scope: Construct, id: string, props: AccessEntryProps ) { super(scope, id); this.cluster = props.cluster; this.principal = props.principal; - this.accessPolicies = props.accessPolicies; - - const resource = new CfnAccessEntry(this, 'Resource', { - clusterName: this.cluster.clusterName, - principalArn: this.principal, - type: props.accessEntryType, - accessPolicies: Lazy.any({ - produce: () => this.accessPolicies.map(p => ({ + this.accessPolicies = props.accessPolicies ?? []; + this.kubernetesGroups = props.kubernetesGroups ?? []; + const accessPolicies = Lazy.any({ + produce: () => { + if (this.accessPolicies.length === 0) { + return undefined; + } + return this.accessPolicies!.map(p => ({ accessScope: { type: p.accessScope.type, namespaces: p.accessScope.namespaces, }, policyArn: p.policy, - })), - }), + })); + }, + }); + const kubernetesGroups = Lazy.list({ + produce: () => { + if (this.kubernetesGroups.length === 0) { + return undefined; + } + return this.kubernetesGroups; + }, + }); + + const resource = new CfnAccessEntry(this, 'Resource', { + clusterName: this.cluster.clusterName, + principalArn: this.principal, + type: props.accessEntryType, + accessPolicies, + kubernetesGroups, }); this.accessEntryName = this.getResourceNameAttribute(resource.ref); this.accessEntryArn = this.getResourceArnAttribute(resource.attrAccessEntryArn, { @@ -361,4 +391,12 @@ export class AccessEntry extends Resource implements IAccessEntry { // add newAccessPolicies to this.accessPolicies this.accessPolicies.push(...newAccessPolicies); } + /** + * Add the access policies for this entry. + * @param newKubernetesGroups - The new kubernetes groups to add. + */ + public addKubernetesGroups(newKubernetesGroups: string[]): void { + // add newKubernetesGroups to this.accessPolicies + this.kubernetesGroups.push(...newKubernetesGroups); + } } diff --git a/packages/aws-cdk-lib/aws-eks/lib/cluster.ts b/packages/aws-cdk-lib/aws-eks/lib/cluster.ts index 8f66f4efa8b3a..da7db1e111240 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/cluster.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { Construct, Node } from 'constructs'; import * as YAML from 'yaml'; -import { IAccessPolicy, IAccessEntry, AccessEntry, AccessPolicy, AccessScopeType } from './access-entry'; +import { IAccessPolicy, IAccessEntry, AccessEntry, AccessPolicy, AccessScopeType, AccessEntryGrants } from './access-entry'; import { IAddon, Addon } from './addon'; import { AlbController, AlbControllerOptions } from './alb-controller'; import { AwsAuth } from './aws-auth'; @@ -725,7 +725,6 @@ interface EndpointAccessConfig { * @default - No restrictions. */ readonly publicCidrs?: string[]; - } /** @@ -1840,7 +1839,26 @@ export class Cluster extends ClusterBase { * @param accessPolicies - An array of `IAccessPolicy` objects that define the access permissions to be granted to the IAM principal. */ public grantAccess(id: string, principal: string, accessPolicies: IAccessPolicy[]) { - this.addToAccessEntry(id, principal, accessPolicies); + this.addToAccessEntry(id, principal, { + accessPolicies: accessPolicies, + }); + } + + /** + * Grants the specified IAM principal access to the EKS cluster based on the provided access policies. + * + * This method creates an `AccessEntry` construct that grants the specified IAM principal the access to + * the specified kubernetesGroups. You have to create (cluster) rolebindings and (cluster) roles manually. + * Without them, the given principal will be able to login, but has no permissions. + * + * @param id - The ID of the `AccessEntry` construct to be created. + * @param principal - The IAM principal (role or user) to be granted access to the EKS cluster. + * @param kubernetesGroups - An array of `IAccessPolicy` objects that define the access permissions to be granted to the IAM principal. + */ + public grantAccessToGroups(id: string, principal: string, kubernetesGroups: string[]) { + this.addToAccessEntry(id, principal, { + kubernetesGroups, + }); } /** @@ -2078,25 +2096,31 @@ export class Cluster extends ClusterBase { /** * Adds an access entry to the cluster's access entries map. * - * If an entry already exists for the given principal, it adds the provided access policies to the existing entry. + * If an entry already exists for the given principal, it adds the provided access policies or kubernetes groups to the existing entry. * If no entry exists for the given principal, it creates a new access entry with the provided access policies. * * @param principal - The principal (e.g., IAM user or role) for which the access entry is being added. - * @param policies - An array of access policies to be associated with the principal. + * @param grants - An object containing arrays of access policies and kubernetes groups to be associated with the principal. * * @throws {Error} If the uniqueName generated for the new access entry is not unique. * * @returns {void} */ - private addToAccessEntry(id: string, principal: string, policies: IAccessPolicy[]) { + private addToAccessEntry(id: string, principal: string, grants: AccessEntryGrants) { const entry = this.accessEntries.get(principal); if (entry) { - (entry as AccessEntry).addAccessPolicies(policies); + if ( grants.accessPolicies ) { + (entry as AccessEntry).addAccessPolicies(grants.accessPolicies); + } + if (grants.kubernetesGroups) { + (entry as AccessEntry).addKubernetesGroups(grants.kubernetesGroups); + } } else { const newEntry = new AccessEntry(this, id, { principal, cluster: this, - accessPolicies: policies, + accessPolicies: grants.accessPolicies, + kubernetesGroups: grants.kubernetesGroups, }); this.accessEntries.set(principal, newEntry); } diff --git a/packages/aws-cdk-lib/aws-eks/test/access-entry.test.ts b/packages/aws-cdk-lib/aws-eks/test/access-entry.test.ts index 5fe74024a5531..edc98b714c98c 100644 --- a/packages/aws-cdk-lib/aws-eks/test/access-entry.test.ts +++ b/packages/aws-cdk-lib/aws-eks/test/access-entry.test.ts @@ -37,7 +37,7 @@ describe('AccessEntry', () => { }; }); - test('creates a new AccessEntry', () => { + test('creates a new AccessEntry with accessPolicies', () => { // WHEN new AccessEntry(stack, 'AccessEntry', { cluster, @@ -61,6 +61,38 @@ describe('AccessEntry', () => { }); }); + test('creates a new AccessEntry with kubernetesGroups', () => { + // WHEN + new AccessEntry(stack, 'AccessEntry', { + cluster, + kubernetesGroups: ['my-kubernetes-group'], + principal: 'mock-principal-arn', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EKS::AccessEntry', { + ClusterName: { Ref: 'Cluster9EE0221C' }, + PrincipalArn: 'mock-principal-arn', + KubernetesGroups: ['my-kubernetes-group'], + }); + }); + + test('creates a new AccessEntry with accessPolicies and kubernetesGroups', () => { + // WHEN + new AccessEntry(stack, 'AccessEntry', { + cluster, + kubernetesGroups: ['my-kubernetes-group'], + principal: 'mock-principal-arn', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EKS::AccessEntry', { + ClusterName: { Ref: 'Cluster9EE0221C' }, + PrincipalArn: 'mock-principal-arn', + KubernetesGroups: ['my-kubernetes-group'], + }); + }); + test.each(Object.values(AccessEntryType))( 'creates a new AccessEntry for AccessEntryType %s', (accessEntryType) => { @@ -95,7 +127,7 @@ describe('AccessEntry', () => { ClusterName: { Ref: 'Cluster9EE0221C' }, PrincipalArn: mockProps.principal, AccessPolicies: [ - { PolicyArn: mockProps.accessPolicies[0].policy }, + { PolicyArn: mockProps.accessPolicies![0].policy }, { AccessScope: { Type: 'cluster', @@ -117,6 +149,20 @@ describe('AccessEntry', () => { }); }); + test('adds new kubernetes groups with addKubernetesGroups()', () => { + // GIVEN + const accessEntry = new AccessEntry(stack, 'AccessEntry', mockProps); + // WHEN + accessEntry.addKubernetesGroups(['my-custom-group']); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EKS::AccessEntry', { + ClusterName: { Ref: 'Cluster9EE0221C' }, + PrincipalArn: mockProps.principal, + KubernetesGroups: ['my-custom-group'], + }); + }); + test('imports an AccessEntry from attributes', () => { // GIVEN const importedAccessEntryName = 'imported-access-entry-name'; diff --git a/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts b/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts index b539eb46d0a34..5de860677c923 100644 --- a/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts +++ b/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts @@ -3364,6 +3364,23 @@ describe('cluster', () => { }); }); + test('cluster can grantAccess to kubernetes group', () => { + // GIVEN + const { stack, vpc } = testFixture(); + // WHEN + const mastersRole = new iam.Role(stack, 'role', { assumedBy: new iam.AccountRootPrincipal() }); + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + mastersRole, + version: CLUSTER_VERSION, + }); + cluster.grantAccessToGroups('mastersAccess', mastersRole.roleArn, ['my-custom-group']); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EKS::AccessEntry', { + KubernetesGroups: ['my-custom-group'], + }); + }); + }); });