in src/skill.ts [93:245]
constructor(scope: Construct, id: string, props: SkillProps) {
// Validate that SSM SecureString was not supplied--Alexa::ASK::Skill does not support SSM SecureString references.
const resolvedClientSecret = cdk.Tokenization.resolve(props.lwaClientSecret, {
scope,
resolver: new cdk.DefaultTokenResolver( new cdk.StringConcat() ),
});
const resolvedRefreshToken = cdk.Tokenization.resolve(props.lwaRefreshToken, {
scope,
resolver: new cdk.DefaultTokenResolver( new cdk.StringConcat() ),
});
if (resolvedClientSecret.includes('ssm-secure')) {
throw new Error('Invalid prop: lwaClientSecret; SSM SecureString is not supported. Use Secrets Manager secret instead.');
}
if (resolvedRefreshToken.includes('ssm-secure')) {
throw new Error('Invalid prop: lwaRefreshToken; SSM SecureString is not supported. Use Secrets Manager secret instead.');
}
super(scope, id);
// Role giving CfnSkill resource read-only access to skill package asset in S3.
const askResourceRole = new iam.Role(this, 'AskResourceRole', {
assumedBy: new iam.ServicePrincipal(ALEXA_SERVICE_PRINCIPAL),
});
// Skill package S3 asset.
const skillPackageAsset = new assets.Asset(this, 'SkillPackageAsset', {
path: props.skillPackagePath,
readers: [askResourceRole],
});
// Alexa Skill with override that injects the endpoint Lambda Function in the skill manifest.
const resource: ask.CfnSkill = new ask.CfnSkill(this, 'Resource', {
vendorId: props.alexaVendorId,
skillPackage: {
s3Bucket: skillPackageAsset.s3BucketName,
s3Key: skillPackageAsset.s3ObjectKey,
s3BucketRole: askResourceRole.roleArn,
...props.endpointLambdaFunction && { // Only add overrides property if endpointLambdaFunction prop was supplied.
overrides: {
manifest: {
apis: {
custom: {
endpoint: {
uri: props.endpointLambdaFunction?.functionArn,
},
},
},
},
},
},
},
authenticationConfiguration: {
clientId: props.lwaClientId,
clientSecret: props.lwaClientSecret.toString(),
refreshToken: props.lwaRefreshToken.toString(),
},
});
// Set resource skillId to Alexa Skill resource Skill ID.
this.skillId = resource.ref;
// This section is only necessary if a Lambda Function was supplied in the props.
if (props.endpointLambdaFunction) {
// Create placeholder Lambda Permission to allow Alexa Skill to pass endpoint validation.
// Permission will be replaced with another containing event source validation after Alexa Skill is created.
const initialLambdaPermission = new lambda.CfnPermission(this, 'InitialLambdaPermission', {
functionName: props.endpointLambdaFunction.functionArn,
principal: ALEXA_SERVICE_PRINCIPAL,
action: BACKEND_LAMBDA_PERMISSION_ACTION,
});
// Skill must be created after the initial Lambda Permission resource is in place to prevent endpoint validation errors.
resource.addDependsOn(initialLambdaPermission);
// Lambda Function that retrieves the StatementId of the initial Lambda Permission for use by other custom resources.
const getPermissionStatementIdFunction = new lambda.Function(this, 'GetLambdaPermissionStatementIdFunction', {
runtime: lambda.Runtime.PYTHON_3_8,
handler: 'index.lambda_handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../custom-resource-runtime/get-lambda-permission-statement-id-handler')),
initialPolicy: [
new iam.PolicyStatement({
actions: ['lambda:GetPolicy'],
resources: [props.endpointLambdaFunction.functionArn],
}),
],
});
// Custom resource for managing lifecycle of GetLambdaPermissionStatementIdFunction Lambda Function.
const getPermissionStatementIdCustomResource = new cdk.CustomResource(this, 'GetLambdaPermissionStatementIdCustomResource', {
serviceToken: new customResources.Provider(this, 'Provider', { onEventHandler: getPermissionStatementIdFunction }).serviceToken,
properties: {
lambda_function_arn: props.endpointLambdaFunction.functionArn,
service_principal_to_match: ALEXA_SERVICE_PRINCIPAL,
action_to_match: BACKEND_LAMBDA_PERMISSION_ACTION,
},
});
// Custom resource code must run after the initial Lambda Permission resource is in place.
getPermissionStatementIdCustomResource.node.addDependency(initialLambdaPermission);
// Get custom resource result for use by other custom resources.
const permissionStatementId = getPermissionStatementIdCustomResource.getAttString('statement_id');
// Policy for AwsCustomResource resources.
const awsCustomResourcePolicy = customResources.AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
actions: [
'lambda:RemovePermission',
'lambda:AddPermission',
],
resources: [props.endpointLambdaFunction.functionArn],
}),
]);
// SDK call to be used for RemovePermissionCustomResource.
const removePermissionStatementSdkCall = {
service: 'Lambda',
action: 'removePermission',
parameters: {
FunctionName: props.endpointLambdaFunction.functionArn,
StatementId: permissionStatementId,
},
ignoreErrorCodesMatching: 'ResourceNotFoundException', // Ignore if there is no matching Permission to remove.
physicalResourceId: customResources.PhysicalResourceId.of(`RemovePermission-${this.skillId}`),
};
const removePermissionCustomResource = new customResources.AwsCustomResource(this, 'RemovePermissionCustomResource', {
policy: awsCustomResourcePolicy,
onCreate: removePermissionStatementSdkCall,
onUpdate: removePermissionStatementSdkCall,
onDelete: removePermissionStatementSdkCall,
});
// RemovePermissionCustomResource code must run after the Alexa Skill has been created to ensure the intial Lambda Permission is in place upon Alexa Skill creation.
removePermissionCustomResource.node.addDependency(resource);
// SDK call to be used for AddPermissionCustomResource.
const addPermissionStatementSdkCall = {
service: 'Lambda',
action: 'addPermission',
parameters: {
FunctionName: props.endpointLambdaFunction.functionArn,
StatementId: permissionStatementId,
Principal: ALEXA_SERVICE_PRINCIPAL,
Action: BACKEND_LAMBDA_PERMISSION_ACTION,
EventSourceToken: this.skillId,
},
physicalResourceId: customResources.PhysicalResourceId.of(`AddPermission-${this.skillId}`),
};
const addPermissionCustomResource = new customResources.AwsCustomResource(this, 'AddPermissionCustomResource', {
policy: awsCustomResourcePolicy,
onCreate: addPermissionStatementSdkCall,
onUpdate: addPermissionStatementSdkCall,
});
// AddPermissionCustomResource code must run after RemovePermissionCustomResource code has run to prevent attempts to create Permission with redundant StatementIds.
addPermissionCustomResource.node.addDependency(removePermissionCustomResource);
}
}