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 Authoriser Based on User Pool Group #617

Merged
merged 10 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
getPierianDxPipelineManagerStackProps,
getPierianDxPipelineTableStackProps,
} from './stacks/pierianDxPipelineManager';
import { getAuthorizationManagerStackProps } from './stacks/authorizationManager';

interface EnvironmentConfig {
name: string;
Expand All @@ -72,6 +73,7 @@ interface EnvironmentConfig {
export const getEnvironmentConfig = (stage: AppStage): EnvironmentConfig | null => {
const stackProps = {
statefulConfig: {
authorizationManagerStackProps: getAuthorizationManagerStackProps(stage),
dataBucketStackProps: getDataBucketStackProps(stage),
sharedStackProps: getSharedStackProps(stage),
postgresManagerStackProps: getPostgresManagerStackProps(),
Expand Down
6 changes: 6 additions & 0 deletions config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const vpcProps: VpcLookupOptions = {
},
};

/**
* The SSM Parameter Name for HTTP Lambda Authorizer ARN (admin user pool group)
*/
export const adminHttpLambdaAuthorizerParameterName =
'/orcabus/authorization-stack/admin-http-lambda-authorization-arn';

// upstream infra: cognito
export const cognitoPortalAppClientIdParameterName =
'/data_portal/client/data2/cog_app_client_id_stage';
Expand Down
21 changes: 21 additions & 0 deletions config/stacks/authorizationManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AuthorizationManagerStackProps } from '../../lib/workload/stateful/stacks/authorization-manager/stack';
import {
cognitoUserPoolIdParameterName,
region,
accountIdAlias,
AppStage,
adminHttpLambdaAuthorizerParameterName,
} from '../constants';

export const getAuthorizationManagerStackProps = (
stage: AppStage
): AuthorizationManagerStackProps => {
return {
cognito: {
userPoolIdParameterName: cognitoUserPoolIdParameterName,
region: region,
accountNumber: accountIdAlias[stage],
},
adminHttpLambdaAuthorizerParameterName: adminHttpLambdaAuthorizerParameterName,
};
};
26 changes: 26 additions & 0 deletions lib/workload/components/api-gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ApiGatewayConstruct

Usage example:

```ts
const apiGateway = new ApiGatewayConstruct(this, 'ApiGateway', props.apiGatewayCognitoProps);
const httpApi = apiGateway.httpApi;

const apiIntegration = new HttpLambdaIntegration('ApiIntegration', apiFn);

new HttpRoute(this, 'GetHttpRoute', {
httpApi: httpApi,
integration: apiIntegration,
routeKey: HttpRouteKey.with('/{proxy+}', HttpMethod.GET),
});

// To protect the path/method with an admin role within the User Pool Cognito
// More details on ./.../stateful/authorization-stack

new HttpRoute(this, 'PostHttpRoute', {
httpApi: httpApi,
integration: apiIntegration,
authorizer: apiGateway.cognitoAdminGroupAuthorizer,
routeKey: HttpRouteKey.with('/{proxy+}', HttpMethod.POST),
});
```
44 changes: 41 additions & 3 deletions lib/workload/components/api-gateway/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Construct } from 'constructs';
import { aws_ssm, Duration, RemovalPolicy } from 'aws-cdk-lib';
import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
import {
HttpJwtAuthorizer,
HttpLambdaAuthorizer,
HttpLambdaResponseType,
} from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
import { CfnStage, CorsHttpMethod, DomainName, HttpApi } from 'aws-cdk-lib/aws-apigatewayv2';
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
import { IStringParameter, StringParameter } from 'aws-cdk-lib/aws-ssm';
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { Function } from 'aws-cdk-lib/aws-lambda';
import { ApiGatewayv2DomainProperties } from 'aws-cdk-lib/aws-route53-targets';
import { adminHttpLambdaAuthorizerParameterName } from '../../../../config/constants';

export interface ApiGwLogsConfig {
/**
Expand Down Expand Up @@ -56,6 +62,7 @@ export interface ApiGatewayConstructProps {
export class ApiGatewayConstruct extends Construct {
private readonly _httpApi: HttpApi;
private readonly _domainName: string;
readonly cognitoAdminGroupAuthorizer: HttpLambdaAuthorizer;

constructor(scope: Construct, id: string, props: ApiGatewayConstructProps) {
super(scope, id);
Expand Down Expand Up @@ -96,12 +103,16 @@ export class ApiGatewayConstruct extends Construct {
allowOrigins: props.corsAllowOrigins,
maxAge: Duration.days(10),
},
defaultAuthorizer: this.getAuthorizer(props),
defaultAuthorizer: this.getJWTAuthorizer(props),
defaultDomainMapping: {
domainName: apiGWDomainName,
},
});

this.cognitoAdminGroupAuthorizer = this.getCognitoAdminGroupHTTPAuthorizer(
adminHttpLambdaAuthorizerParameterName
);

new ARecord(this, 'CustomDomainARecord', {
zone: HostedZone.fromHostedZoneAttributes(this, 'UmccrHostedZone', {
hostedZoneId,
Expand Down Expand Up @@ -160,7 +171,7 @@ export class ApiGatewayConstruct extends Construct {
accessLogs.grantWrite(role);
}

private getAuthorizer(props: ApiGatewayConstructProps): HttpJwtAuthorizer {
private getJWTAuthorizer(props: ApiGatewayConstructProps): HttpJwtAuthorizer {
/**
* FIXME One fine day in future when we have proper Cognito AAI setup.
* For the moment, we leverage Portal and established Cognito infrastructure.
Expand Down Expand Up @@ -190,6 +201,33 @@ export class ApiGatewayConstruct extends Construct {
});
}

/**
* Get the Cognito Admin Group HTTP Lambda Authorizer
* @param adminHttpLambdaAuthorizerParameterName The SSM Parameter Name that stores the ARN of the lambda authorizer
* @returns
*/
private getCognitoAdminGroupHTTPAuthorizer(adminHttpLambdaAuthorizerParameterName: string) {
const lambdaArn = StringParameter.valueForStringParameter(
this,
adminHttpLambdaAuthorizerParameterName
);

// Get the lambda HTTP authorizer defined in the authorization stack manager
const lambdaAuthorizer = Function.fromFunctionAttributes(
this,
'AdminGroupHTTPAuthorizerLambda',
{
functionArn: lambdaArn,
sameEnvironment: true,
}
);

return new HttpLambdaAuthorizer('AdminGroupLambdaAuthorizer', lambdaAuthorizer, {
authorizerName: 'CognitoAdminGroupLambdaAuthorizer',
responseTypes: [HttpLambdaResponseType.SIMPLE],
});
}

get httpApi(): HttpApi {
return this._httpApi;
}
Expand Down
26 changes: 26 additions & 0 deletions lib/workload/stateful/stacks/authorization-manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Authorization Stack

This stack contains resources that handle authorization requests.

## AWS Verified Permissions

The current stack deploys AWS Verified Permissions, defining an identity source and policies as described below. A HTTP Lambda Authorizer is also included for use with other stacks.

### Identity Source

- **UMCCR Cognito User Pool**

Sourced from the UMCCR Cognito User Pool, defined in the infrastructure Terraform repository. The AWS Cognito User Pool
is expected to have an `admin` group, which will be used in the policy. Note that the JWT must be generated with the
latest token containing the proper Cognito group claims for it to work. This also applies when a user is removed from
the group; the JWT must expire to become invalid.

### Policy

- **AdminPolicy**

A static policy defined in the stack that allows anyone in the `admin` group of the Cognito user pool to perform any
action. This essentially checks if a user is in the `admin` group, integrated with the Cognito setup.

The HTTP Lambda Authorizer is also defined for use in stacks where routes/methods need to comply with this policy. The
Lambda ARN is stored in SSM Parameter String defined in `config/constants.ts` as the `adminHttpLambdaAuthorizerParameterName` constant.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"OrcaBus": {
"actions": {
"writeAccess": {
"appliesTo": {
"principalTypes": ["User"]
}
}
},
"entityTypes": {
"CognitoUserGroup": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {}
}
},
"User": {
"memberOfTypes": ["CognitoUserGroup"],
"shape": {
"attributes": {
"email": {
"type": "String",
"required": false
}
},
"type": "Record"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
import boto3

client = boto3.client('verifiedpermissions')


def handler(event, context):
response = {
"isAuthorized": False,
}

auth_token = event["headers"]["authorization"]

try:

if auth_token:
avp_response = client.is_authorized_with_token(
policyStoreId=os.environ.get("POLICY_STORE_ID"),
identityToken=auth_token,
)

return {
"isAuthorized": avp_response.get("decision", None) == "ALLOW",
}
else:
return response
except BaseException:
return response
135 changes: 135 additions & 0 deletions lib/workload/stateful/stacks/authorization-manager/stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { CfnPolicyStore, CfnPolicy, CfnIdentitySource } from 'aws-cdk-lib/aws-verifiedpermissions';
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';

import cedarSchemaJson from './cedarSchema.json';
import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda';
import path from 'path';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';

export interface AuthorizationManagerStackProps {
cognito: CognitoConfig;
adminHttpLambdaAuthorizerParameterName: string;
}

interface CognitoConfig {
/**
* The SSM parameter name that cognito user pool ID is stored
*/
userPoolIdParameterName: string;
/**
* The AWS region where the cognito user pool is deployed
*/
region: string;
/**
* The AWS account number where the cognito user pool is deployed
*/
accountNumber: string;
}

export class AuthorizationManagerStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps & AuthorizationManagerStackProps) {
super(scope, id, props);

// Amazon Verified Permission
const policyStore = new CfnPolicyStore(this, 'VerifiedPermissionPolicyStore', {
validationSettings: { mode: 'STRICT' },
description: 'OrcaBus authorization policy',
schema: {
cedarJson: JSON.stringify(cedarSchemaJson),
},
});

this.setupCognitoIntegrationAndPolicy({
cognito: props.cognito,
policyStoreId: policyStore.attrPolicyStoreId,
});

this.setupTokenLambdaAuthorization({
policyStoreARN: policyStore.attrArn,
policyStoreId: policyStore.attrPolicyStoreId,
adminHttpLambdaAuthorizerParameterName: props.adminHttpLambdaAuthorizerParameterName,
});
}

/**
* This sets up the Verified Permissions integration with Cognito.
* It sources users from the Cognito user pool and creates a static policy
* that grants all permissions to users in the admin group within the user pool.
*
* @param props Cognito properties
*/
private setupCognitoIntegrationAndPolicy(props: {
policyStoreId: string;
cognito: CognitoConfig;
}) {
// Grab the user pool ID from SSM
const userPoolId = StringParameter.fromStringParameterName(
this,
'CognitoUserPoolIdStringParameter',
props.cognito.userPoolIdParameterName
).stringValue;

// Allow the policy store to source the identity from existing Cognito User Pool Id
new CfnIdentitySource(this, 'VerifiedPermissionIdentitySource', {
configuration: {
cognitoUserPoolConfiguration: {
userPoolArn: `arn:aws:cognito-idp:${props.cognito.region}:${props.cognito.accountNumber}:userpool/${userPoolId}`,
groupConfiguration: {
groupEntityType: 'OrcaBus::CognitoUserGroup', // Refer to './cedarSchema.json'
},
},
},
principalEntityType: 'OrcaBus::User',
policyStoreId: props.policyStoreId,
});

// Create a static policy that allow user from the admin group to allow all actions
new CfnPolicy(this, 'CognitoPortalAdminPolicy', {
definition: {
static: {
statement: `
permit (
principal in OrcaBus::CognitoUserGroup::"${userPoolId}|admin",
action,
resource
);
`,
description:
'Allow all action for all resource for user in the admin cognito user pool group',
},
},
policyStoreId: props.policyStoreId,
});
}

private setupTokenLambdaAuthorization(props: {
policyStoreId: string;
policyStoreARN: string;
adminHttpLambdaAuthorizerParameterName: string;
}) {
const adminLambdaAuth = new PythonFunction(this, 'AdminHTTPAuthorizerLambda', {
entry: path.join(__dirname, 'http-lambda-authorizer'),
architecture: Architecture.ARM_64,
runtime: Runtime.PYTHON_3_12,
index: 'admin_access_authorizer.py',
retryAttempts: 0,
environment: { POLICY_STORE_ID: props.policyStoreId },
initialPolicy: [
new PolicyStatement({
actions: ['verifiedpermissions:IsAuthorizedWithToken'],
resources: [props.policyStoreARN],
}),
],
});

new StringParameter(this, 'AdminHTTPAuthorizerLambdaARNParameter', {
parameterName: props.adminHttpLambdaAuthorizerParameterName,
description:
'ARN of the HTTP lambda authorizer that allow access for admin in the cognito user pool group',
stringValue: adminLambdaAuth.functionArn,
});
}
}
Loading