Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add kubernetesGroups to AccessEntries #32074

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
54 changes: 52 additions & 2 deletions packages/aws-cdk-lib/aws-eks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
68 changes: 53 additions & 15 deletions packages/aws-cdk-lib/aws-eks/lib/access-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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, {
Expand All @@ -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);
}
}
40 changes: 32 additions & 8 deletions packages/aws-cdk-lib/aws-eks/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -726,7 +726,6 @@ interface EndpointAccessConfig {
* @default - No restrictions.
*/
readonly publicCidrs?: string[];

}

/**
Expand Down Expand Up @@ -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,
});
}

/**
Expand Down Expand Up @@ -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);
}
Expand Down
50 changes: 48 additions & 2 deletions packages/aws-cdk-lib/aws-eks/test/access-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('AccessEntry', () => {
};
});

test('creates a new AccessEntry', () => {
test('creates a new AccessEntry with accessPolicies', () => {
// WHEN
new AccessEntry(stack, 'AccessEntry', {
cluster,
Expand All @@ -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) => {
Expand Down Expand Up @@ -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',
Expand All @@ -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';
Expand Down
17 changes: 17 additions & 0 deletions packages/aws-cdk-lib/aws-eks/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
});
});

});

});
Loading