From e97d3c85e33bd7831b42d9531cfb7a4292789b42 Mon Sep 17 00:00:00 2001 From: Markus Date: Sat, 9 Nov 2024 08:27:07 +0100 Subject: [PATCH 1/5] feat: add kubernetesGroups to AccessEntries --- .../aws-cdk-lib/aws-eks/lib/access-entry.ts | 35 ++++++++++----- .../aws-eks/test/access-entry.test.ts | 44 ++++++++++++++++++- 2 files changed, 67 insertions(+), 12 deletions(-) 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 8901bc2a4a59f..cab65da4fada7 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts @@ -281,11 +281,16 @@ export interface AccessEntryProps { /** * The access policies that define the permissions and scope for the access entry. */ - readonly accessPolicies: IAccessPolicy[]; + readonly accessPolicies?: IAccessPolicy[]; /** * The Amazon Resource Name (ARN) of the principal (user or role) to associate the access entry with. */ readonly principal: string; + /** + * The kubernetes groups you want to associate with this access policy. + * Those groups can be used as subjects in (Cluster)RoleBindings. + */ + readonly kubernetesGroups?: string[]; } /** @@ -323,28 +328,36 @@ 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 resource = new CfnAccessEntry(this, 'Resource', { + clusterName: this.cluster.clusterName, + principalArn: this.principal, + type: props.accessEntryType, + accessPolicies, + kubernetesGroups: this.kubernetesGroups, }); this.accessEntryName = this.getResourceNameAttribute(resource.ref); this.accessEntryArn = this.getResourceArnAttribute(resource.attrAccessEntryArn, { 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 ce36757b2c18d..9419e97bc06e7 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,48 @@ describe('AccessEntry', () => { }); }); + test('creates a new AccessEntry with kubernetesGroups', () => { + // WHEN + new AccessEntry(stack, 'AccessEntry', { + cluster, + kubernetesGroups: ['my-kubernetes-group'], + accessPolicies: mockAccessPolicies, + principal: 'mock-principal-arn', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EKS::AccessEntry', { + ClusterName: { Ref: 'Cluster9EE0221C' }, + PrincipalArn: 'mock-principal-arn', + AccessPolicies: [ + { + AccessScope: { + Namespaces: ['default'], + Type: 'namespace', + }, + PolicyArn: 'mock-policy-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) => { From 52b846107b69e9383b26ec3ce11d31f258672ed1 Mon Sep 17 00:00:00 2001 From: Markus Date: Sat, 9 Nov 2024 08:38:49 +0100 Subject: [PATCH 2/5] chore: fix test --- packages/aws-cdk-lib/aws-eks/test/access-entry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9419e97bc06e7..3f94a0452259c 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 @@ -137,7 +137,7 @@ describe('AccessEntry', () => { ClusterName: { Ref: 'Cluster9EE0221C' }, PrincipalArn: mockProps.principal, AccessPolicies: [ - { PolicyArn: mockProps.accessPolicies[0].policy }, + { PolicyArn: mockProps.accessPolicies![0].policy }, { AccessScope: { Type: 'cluster', From 6821c965bbf61da53c593d1660f2e641030bc59b Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 14 Nov 2024 20:48:57 +0100 Subject: [PATCH 3/5] fix: defaults --- packages/aws-cdk-lib/aws-eks/lib/access-entry.ts | 2 ++ 1 file changed, 2 insertions(+) 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 cab65da4fada7..fddd3febd86cb 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts @@ -280,6 +280,7 @@ export interface AccessEntryProps { readonly cluster: ICluster; /** * The access policies that define the permissions and scope for the access entry. + * @default [] */ readonly accessPolicies?: IAccessPolicy[]; /** @@ -289,6 +290,7 @@ export interface AccessEntryProps { /** * The kubernetes groups you want to associate with this access policy. * Those groups can be used as subjects in (Cluster)RoleBindings. + * @default undefined */ readonly kubernetesGroups?: string[]; } From 4c6824c75c182a84546e93ab4fb3dccfa0881f17 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 14 Nov 2024 21:27:13 +0100 Subject: [PATCH 4/5] fix --- packages/aws-cdk-lib/aws-eks/lib/access-entry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fddd3febd86cb..4825fc6835f6c 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/access-entry.ts @@ -280,7 +280,7 @@ export interface AccessEntryProps { readonly cluster: ICluster; /** * The access policies that define the permissions and scope for the access entry. - * @default [] + * @default - No access policies are provided */ readonly accessPolicies?: IAccessPolicy[]; /** @@ -290,7 +290,7 @@ export interface AccessEntryProps { /** * The kubernetes groups you want to associate with this access policy. * Those groups can be used as subjects in (Cluster)RoleBindings. - * @default undefined + * @default - No kubernetes groups are provided */ readonly kubernetesGroups?: string[]; } From a56db8f8b0cd35c44c71ad1fca45383c544f71a5 Mon Sep 17 00:00:00 2001 From: Markus Date: Sun, 17 Nov 2024 18:56:30 +0100 Subject: [PATCH 5/5] feat: more docs and code --- packages/aws-cdk-lib/aws-eks/README.md | 54 ++++++++++++++++++- .../aws-cdk-lib/aws-eks/lib/access-entry.ts | 53 ++++++++++++------ packages/aws-cdk-lib/aws-eks/lib/cluster.ts | 40 +++++++++++--- .../aws-eks/test/access-entry.test.ts | 24 +++++---- .../aws-cdk-lib/aws-eks/test/cluster.test.ts | 17 ++++++ 5 files changed, 153 insertions(+), 35 deletions(-) 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 4b3b5241cddb8..b5b87bed81cb5 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,21 +295,10 @@ 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. - * @default - No access policies are provided - */ - readonly accessPolicies?: IAccessPolicy[]; /** * The Amazon Resource Name (ARN) of the principal (user or role) to associate the access entry with. */ readonly principal: string; - /** - * 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[]; } /** @@ -330,7 +336,7 @@ export class AccessEntry extends Resource implements IAccessEntry { private cluster: ICluster; private principal: string; private accessPolicies: IAccessPolicy[]; - private kubernetesGroups?: string[]; + private kubernetesGroups: string[]; constructor(scope: Construct, id: string, props: AccessEntryProps ) { super(scope, id); @@ -338,7 +344,7 @@ export class AccessEntry extends Resource implements IAccessEntry { this.cluster = props.cluster; this.principal = props.principal; this.accessPolicies = props.accessPolicies ?? []; - this.kubernetesGroups = props.kubernetesGroups; + this.kubernetesGroups = props.kubernetesGroups ?? []; const accessPolicies = Lazy.any({ produce: () => { if (this.accessPolicies.length === 0) { @@ -354,12 +360,21 @@ export class AccessEntry extends Resource implements IAccessEntry { }, }); + 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.kubernetesGroups, + kubernetesGroups, }); this.accessEntryName = this.getResourceNameAttribute(resource.ref); this.accessEntryArn = this.getResourceArnAttribute(resource.attrAccessEntryArn, { @@ -376,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 2c624a477c9b8..ffbc2fe69bde1 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/cluster.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Construct, Node } from 'constructs'; import * as semver from 'semver'; 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'; @@ -726,7 +726,6 @@ interface EndpointAccessConfig { * @default - No restrictions. */ readonly publicCidrs?: string[]; - } /** @@ -1842,7 +1841,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, + }); } /** @@ -2080,25 +2098,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 3f94a0452259c..b039708170dea 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 @@ -66,7 +66,6 @@ describe('AccessEntry', () => { new AccessEntry(stack, 'AccessEntry', { cluster, kubernetesGroups: ['my-kubernetes-group'], - accessPolicies: mockAccessPolicies, principal: 'mock-principal-arn', }); @@ -74,15 +73,6 @@ describe('AccessEntry', () => { Template.fromStack(stack).hasResourceProperties('AWS::EKS::AccessEntry', { ClusterName: { Ref: 'Cluster9EE0221C' }, PrincipalArn: 'mock-principal-arn', - AccessPolicies: [ - { - AccessScope: { - Namespaces: ['default'], - Type: 'namespace', - }, - PolicyArn: 'mock-policy-arn', - }, - ], KubernetesGroups: ['my-kubernetes-group'], }); }); @@ -159,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 65002fb5909e4..866dab4579d92 100644 --- a/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts +++ b/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts @@ -3378,6 +3378,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'], + }); + }); + }); }); \ No newline at end of file