Skip to content

Commit

Permalink
update iauth interface to include function for creating admin user
Browse files Browse the repository at this point in the history
  • Loading branch information
suhussai committed Jun 13, 2024
1 parent ff62f29 commit e6178cb
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 99 deletions.
22 changes: 22 additions & 0 deletions src/control-plane/auth/auth-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@

import { SecretValue } from 'aws-cdk-lib';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

/**
* Encapsulates the list of properties expected as inputs for creating
* new admin users.
*/
export interface CreateAdminUserProps {
/**
* The email address of the new admin user.
*/
readonly email: string;

/**
* The name of the role of the new admin user.
*/
readonly role: string;
}

/**
* Encapsulates the list of properties expected as outputs of Auth plugins
Expand Down Expand Up @@ -169,4 +186,9 @@ export interface IAuth {
* The Lambda function for enabling a user. -- PUT /user/{userId}/enable
*/
readonly enableUserFunction: IFunction;

/**
* Function to create an admin user.
*/
createAdminUser(scope: Construct, id: string, props: CreateAdminUserProps): void;
}
198 changes: 101 additions & 97 deletions src/control-plane/auth/cognito-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,22 @@ import {
ServicePrincipal,
Effect,
} from 'aws-cdk-lib/aws-iam';
import { Runtime, IFunction, LayerVersion } from 'aws-cdk-lib/aws-lambda';
import { Runtime, IFunction, LayerVersion, ILayerVersion } from 'aws-cdk-lib/aws-lambda';
import { NagSuppressions } from 'cdk-nag';
import { Construct } from 'constructs';
import { IAuth } from './auth-interface';
import { CreateAdminUserProps, IAuth } from './auth-interface';
import { addTemplateTag } from '../../utils';

/**
* Properties for the CognitoAuth construct.
*/
export interface CognitoAuthProps {
/**
* The email address of the system admin.
*/
readonly systemAdminEmail: string;

/**
* The callback URL for the control plane.
* @default 'http://localhost'
*/
readonly controlPlaneCallbackURL?: string;

/**
* The name of the system admin role.
* @default 'SystemAdmin'
*/
readonly systemAdminRoleName?: string;

/**
* Whether or not to specify scopes for validation at the API GW.
* Can be used for testing purposes.
Expand Down Expand Up @@ -214,70 +203,34 @@ export class CognitoAuth extends Construct implements IAuth {
*/
readonly enableUserFunction: IFunction;

/**
* UserPool created as part of this construct.
*/
private readonly userPool: cognito.UserPool;

/**
* The Lambda Layer containing the Powertools library.
*/
private readonly lambdaPowertoolsLayer: ILayerVersion;

/**
* The IAM Role for Lambda that enables creating admin users.
*/
private readonly lambdaIdpExecRole: Role;

constructor(scope: Construct, id: string, props: CognitoAuthProps) {
super(scope, id);
addTemplateTag(this, 'CognitoAuth');

const systemAdminRoleName = props.systemAdminRoleName ?? 'SystemAdmin';
const defaultControlPlaneCallbackURL = 'http://localhost';

// https://docs.powertools.aws.dev/lambda/python/2.31.0/#lambda-layer
const lambdaPowertoolsLayer = LayerVersion.fromLayerVersionArn(
this.lambdaPowertoolsLayer = LayerVersion.fromLayerVersionArn(
this,
'LambdaPowerTools',
`arn:aws:lambda:${Stack.of(this).region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:59`
);
const defaultControlPlaneCallbackURL = 'http://localhost';

const lambdaIdpExecRole = new Role(this, 'lambdaIdpExecRole', {
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
});

lambdaIdpExecRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
);
lambdaIdpExecRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy')
);
lambdaIdpExecRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName('AWSXrayWriteOnlyAccess')
);

lambdaIdpExecRole.addToPolicy(
new PolicyStatement({
actions: [
'cognito-idp:AdminCreateUser',
'cognito-idp:CreateGroup',
'cognito-idp:AdminAddUserToGroup',
'cognito-idp:GetGroup',
'cognito-idp:DescribeUserPool',
],
effect: Effect.ALLOW,
resources: ['*'],
})
);

NagSuppressions.addResourceSuppressions(
lambdaIdpExecRole,
[
{
id: 'AwsSolutions-IAM5',
reason: 'Auth resource name(s) not known beforehand.',
},
{
id: 'AwsSolutions-IAM4',
reason:
'Suppress usage of AWSLambdaBasicExecutionRole, CloudWatchLambdaInsightsExecutionRolePolicy, and AWSXrayWriteOnlyAccess.',
appliesTo: [
'Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
'Policy::arn:<AWS::Partition>:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy',
'Policy::arn:<AWS::Partition>:iam::aws:policy/AWSXrayWriteOnlyAccess',
],
},
],
true // applyToChildren = true, so that it applies to policies created for the role.
);

const userPool = new cognito.UserPool(this, 'UserPool', {
this.userPool = new cognito.UserPool(this, 'UserPool', {
autoVerify: { email: true },
passwordPolicy: {
minLength: 8,
Expand All @@ -300,7 +253,7 @@ export class CognitoAuth extends Construct implements IAuth {
advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
});

NagSuppressions.addResourceSuppressions(userPool, [
NagSuppressions.addResourceSuppressions(this.userPool, [
{
id: 'AwsSolutions-COG2',
reason: 'Not requiring MFA at this phase.',
Expand All @@ -317,7 +270,7 @@ export class CognitoAuth extends Construct implements IAuth {
scopeDescription: 'Write access to users.',
});

const userResourceServer = userPool.addResourceServer('UserResourceServer', {
const userResourceServer = this.userPool.addResourceServer('UserResourceServer', {
identifier: 'user',
scopes: [readUserScope, writeUserScope],
});
Expand Down Expand Up @@ -352,7 +305,7 @@ export class CognitoAuth extends Construct implements IAuth {
scopeDescription: 'Write access to tenants.',
});

const tenantResourceServer = userPool.addResourceServer('TenantResourceServer', {
const tenantResourceServer = this.userPool.addResourceServer('TenantResourceServer', {
identifier: 'tenant',
scopes: [readTenantScope, writeTenantScope],
});
Expand All @@ -379,14 +332,14 @@ export class CognitoAuth extends Construct implements IAuth {

// Create a Cognito User Pool Domain
const userPoolDomain = new cognito.UserPoolDomain(this, 'UserPoolDomain', {
userPool: userPool,
userPool: this.userPool,
cognitoDomain: {
domainPrefix: `saascontrolplane-${this.node.addr}`,
},
});

const userPoolMachineClient = new cognito.UserPoolClient(this, 'UserPoolMachineClient', {
userPool: userPool,
userPool: this.userPool,
generateSecret: true,
authFlows: {
userPassword: false,
Expand All @@ -403,7 +356,7 @@ export class CognitoAuth extends Construct implements IAuth {
});

const userPoolUserClient = new cognito.UserPoolClient(this, 'UserPoolUserClient', {
userPool: userPool,
userPool: this.userPool,
generateSecret: false,
authFlows: {
userPassword: true,
Expand Down Expand Up @@ -432,39 +385,20 @@ export class CognitoAuth extends Construct implements IAuth {
.withCustomAttributes('userRole'),
});

const createAdminUserFunction = new PythonFunction(this, 'createAdminUserFunction', {
entry: path.join(__dirname, '../../../resources/functions/auth-custom-resource'),
runtime: Runtime.PYTHON_3_12,
index: 'index.py',
handler: 'handler',
timeout: Duration.seconds(60),
role: lambdaIdpExecRole,
layers: [lambdaPowertoolsLayer],
});

new CustomResource(this, 'createAdminUserCustomResource', {
serviceToken: createAdminUserFunction.functionArn,
properties: {
UserPoolId: userPool.userPoolId,
SystemAdminRoleName: systemAdminRoleName,
SystemAdminEmail: props.systemAdminEmail,
},
});

const region = cdk.Stack.of(this).region;
this.userClientId = userPoolUserClient.userPoolClientId;
this.machineClientId = userPoolMachineClient.userPoolClientId;
this.machineClientSecret = userPoolMachineClient.userPoolClientSecret;
this.wellKnownEndpointUrl = `https://cognito-idp.${region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`;
this.jwtIssuer = `https://cognito-idp.${region}.amazonaws.com/${userPool.userPoolId}`;
this.wellKnownEndpointUrl = `https://cognito-idp.${region}.amazonaws.com/${this.userPool.userPoolId}/.well-known/openid-configuration`;
this.jwtIssuer = `https://cognito-idp.${region}.amazonaws.com/${this.userPool.userPoolId}`;
this.jwtAudience = [
userPoolUserClient.userPoolClientId,
userPoolMachineClient.userPoolClientId,
];
this.tokenEndpoint = `https://${userPoolDomain.domainName}.auth.${region}.amazoncognito.com/oauth2/token`;

new cdk.CfnOutput(this, 'ControlPlaneIdpUserPoolId', {
value: userPool.userPoolId,
value: this.userPool.userPoolId,
key: 'ControlPlaneIdpUserPoolId',
});

Expand Down Expand Up @@ -532,9 +466,9 @@ export class CognitoAuth extends Construct implements IAuth {
handler: 'lambda_handler',
timeout: Duration.seconds(60),
role: userManagementExecRole,
layers: [lambdaPowertoolsLayer],
layers: [this.lambdaPowertoolsLayer],
environment: {
USER_POOL_ID: userPool.userPoolId,
USER_POOL_ID: this.userPool.userPoolId,
},
});

Expand Down Expand Up @@ -567,5 +501,75 @@ export class CognitoAuth extends Construct implements IAuth {
},
]
);

this.lambdaIdpExecRole = new Role(this, 'lambdaIdpExecRole', {
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
});

this.lambdaIdpExecRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
);
this.lambdaIdpExecRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy')
);
this.lambdaIdpExecRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName('AWSXrayWriteOnlyAccess')
);

this.lambdaIdpExecRole.addToPolicy(
new PolicyStatement({
actions: [
'cognito-idp:AdminCreateUser',
'cognito-idp:CreateGroup',
'cognito-idp:AdminAddUserToGroup',
'cognito-idp:GetGroup',
'cognito-idp:DescribeUserPool',
],
effect: Effect.ALLOW,
resources: ['*'],
})
);

NagSuppressions.addResourceSuppressions(
this.lambdaIdpExecRole,
[
{
id: 'AwsSolutions-IAM5',
reason: 'Auth resource name(s) not known beforehand.',
},
{
id: 'AwsSolutions-IAM4',
reason:
'Suppress usage of AWSLambdaBasicExecutionRole, CloudWatchLambdaInsightsExecutionRolePolicy, and AWSXrayWriteOnlyAccess.',
appliesTo: [
'Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
'Policy::arn:<AWS::Partition>:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy',
'Policy::arn:<AWS::Partition>:iam::aws:policy/AWSXrayWriteOnlyAccess',
],
},
],
true // applyToChildren = true, so that it applies to policies created for the role.
);
}

createAdminUser(scope: Construct, id: string, props: CreateAdminUserProps) {
const createAdminUserFunction = new PythonFunction(scope, `createAdminUserFunction-${id}`, {
entry: path.join(__dirname, '../../../resources/functions/auth-custom-resource'),
runtime: Runtime.PYTHON_3_12,
index: 'index.py',
handler: 'handler',
timeout: Duration.seconds(60),
role: this.lambdaIdpExecRole,
layers: [this.lambdaPowertoolsLayer],
});

new CustomResource(scope, `createAdminUserCustomResource-${id}`, {
serviceToken: createAdminUserFunction.functionArn,
properties: {
UserPoolId: this.userPool.userPoolId,
SystemAdminEmail: props.email,
SystemAdminRoleName: props.role,
},
});
}
}
18 changes: 18 additions & 0 deletions src/control-plane/control-plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export interface ControlPlaneProps {
*/
readonly auth: IAuth;

/**
* The email address of the system admin.
*/
readonly systemAdminEmail: string;

/**
* The name of the system admin role.
* @default 'SystemAdmin'
*/
readonly systemAdminRoleName?: string;

/**
* The billing provider configuration.
*/
Expand Down Expand Up @@ -57,8 +68,15 @@ export class ControlPlane extends Construct {
super(scope, id);
addTemplateTag(this, 'ControlPlane');

const systemAdminRoleName = props.systemAdminRoleName || 'SystemAdmin';

cdk.Aspects.of(this).add(new DestroyPolicySetter());

props.auth.createAdminUser(this, 'adminUser', {
email: props.systemAdminEmail,
role: systemAdminRoleName,
});

// todo: decompose 'Tables' into purpose-specific constructs (ex. TenantManagement)
this.tables = new Tables(this, 'tables-stack');

Expand Down
4 changes: 2 additions & 2 deletions src/control-plane/integ.default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export class IntegStack extends cdk.Stack {
super(scope, id, props);

const cognitoAuth = new CognitoAuth(this, 'CognitoAuth', {
systemAdminEmail: props.systemAdminEmail,
setAPIGWScopes: false,
setAPIGWScopes: false, // optional parameter
});

const eventManager = new EventManager(this, 'EventManager');

const controlPlane = new ControlPlane(this, 'ControlPlane', {
systemAdminEmail: props.systemAdminEmail,
auth: cognitoAuth,
eventManager: eventManager,
});
Expand Down

0 comments on commit e6178cb

Please sign in to comment.