Skip to content

Commit

Permalink
use sdk v3
Browse files Browse the repository at this point in the history
  • Loading branch information
badmintoncryer committed Oct 22, 2024
1 parent f1f7f11 commit e682875
Show file tree
Hide file tree
Showing 12 changed files with 1,426 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const project = new awscdk.AwsCdkConstructLibrary({

keywords: ['aws', 'cdk', 'ec2', 'aws-cdk'],
gitignore: ['*.js', '*.d.ts', '!test/.*.snapshot/**/*', '.tmp'],
deps: [],
deps: ['aws-lambda', '@aws-sdk/client-iot'],
description: 'CDK Construct for AWS IoT Core certificates and things',
devDeps: [
'@aws-cdk/integ-runner@2.80.0-alpha.0',
Expand Down
4 changes: 4 additions & 0 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 95 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,96 @@
export class Hello {
public sayHello() {
return 'hello, world!';
import { join } from 'node:path';
import { Duration, ResourceProps } from 'aws-cdk-lib';
import { CfnCustomResource } from 'aws-cdk-lib/aws-cloudformation';
import { CompositePrincipal, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import { CfnParameter } from 'aws-cdk-lib/aws-ssm';
import { Provider } from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';

export interface ThingWithCertProps extends ResourceProps {
readonly thingName: string;
readonly saveToParamStore?: boolean;
readonly paramPrefix?: string;
}

export class ThingWithCert extends Construct {
public readonly thingArn: string;
public readonly certId: string;
public readonly certPem: string;
public readonly privKey: string;
constructor(scope: Construct, id: string, props: ThingWithCertProps) {
super(scope, id);

const { thingName, saveToParamStore, paramPrefix } = props;

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

role.addToPolicy(
new PolicyStatement({
resources: ['arn:aws:logs:*:*:*'],
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
}),
);

role.addToPolicy(
new PolicyStatement({
resources: ['*'],
actions: ['iot:*'],
}),
);

const onEventHandler = new NodejsFunction(this, 'lambdaFunction', {
entry: join(__dirname, 'lambda', 'index.js'),
handler: 'handler',
timeout: Duration.seconds(10),
role,
logRetention: RetentionDays.ONE_DAY,
});

const { serviceToken } = new Provider(this, 'lambdaProvider', {
onEventHandler,
});

const lambdaCustomResource = new CfnCustomResource(this, 'lambdaCustomResource', {
serviceToken,
});

lambdaCustomResource.addPropertyOverride('ThingName', thingName);

const paramStorePath = getParamStorePath(thingName, paramPrefix);

if (saveToParamStore) {
new CfnParameter(this, 'paramStoreCertPem', {
type: 'String',
value: lambdaCustomResource.getAtt('certPem').toString(),
name: `${paramStorePath}/certPem`,
});

new CfnParameter(this, 'paramStorePrivKey', {
type: 'String',
value: lambdaCustomResource.getAtt('privKey').toString(),
name: `${paramStorePath}/privKey`,
});
}

this.thingArn = lambdaCustomResource.getAtt('thingArn').toString();
this.certId = lambdaCustomResource.getAtt('certId').toString();
this.certPem = lambdaCustomResource.getAtt('certPem').toString();
this.privKey = lambdaCustomResource.getAtt('privKey').toString();
}
}
}

export const getParamStorePath = (thingName: string, paramPrefix?: string) => {
if (thingName.charAt(0) === '/') {
throw new Error("thingName cannot start with '/'");
}

if (paramPrefix && paramPrefix.charAt(0) === '/') {
throw new Error("paramPrefix cannot start with '/'");
}

return paramPrefix ? `/${paramPrefix}/${thingName}` : `/${thingName}`;
};
96 changes: 96 additions & 0 deletions src/lambda/adapters/iot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { IoT } from '@aws-sdk/client-iot';
import { IotPort } from '../ports/iot';
import { getCertIdFromARN } from '../util/iot';

export const iotAdaptor = (iot: IoT): IotPort => {
return {
createThing: async (thingRequest) => {
return iot.createThing(thingRequest);
},
createKeysAndCertificates: async () => {
return iot
.createKeysAndCertificate({
setAsActive: true,
});
},
createPolicy: async (thingName) => {
return iot
.createPolicy({
policyName: thingName,
policyDocument: policyDoc,
});
},
attachPrincipalPolicy: async (props) => {
await iot.attachPrincipalPolicy(props);
},
attachThingPrincipal: async (props) => {
return iot.attachThingPrincipal(props);
},
listThingPrincipals: async (thingName) => {
return iot
.listThingPrincipals({
thingName: thingName,
});
},
detachPrincipalPolicy: async (props) => {
await iot.detachPrincipalPolicy(props);
},
detachThingPrincipal: async (props) => {
return iot.detachThingPrincipal(props);
},
updateCertificateToInactive: async (certArn) => {
await iot
.updateCertificate({
certificateId: getCertIdFromARN(certArn),
newStatus: 'INACTIVE',
});
},
deleteCertificate: async (certArn) => {
await iot
.deleteCertificate({
certificateId: getCertIdFromARN(certArn),
});
},
deletePolicy: async (policyName) => {
await iot
.deletePolicy({
policyName: policyName,
});
},
deleteThing: async (thingName) => {
await iot
.deleteThing({
thingName: thingName,
});
},
};
};

const policyDoc = `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iot:Publish",
"iot:Subscribe",
"iot:Connect",
"iot:Receive"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"iot:GetThingShadow",
"iot:UpdateThingShadow",
"iot:DeleteThingShadow"
],
"Resource": [
"*"
]
}
]
}`;
58 changes: 58 additions & 0 deletions src/lambda/adapters/thing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { IotPort } from '../ports/iot';
import { ThingPort } from '../ports/thing';

export const thingAdaptor = (iotAdaptor: IotPort): ThingPort => {
return {
create: async (thingName) => {
const { thingArn } = await iotAdaptor.createThing({
thingName: thingName,
});
console.info(`Thing created with ARN: ${thingArn}`);
const { certificateId, certificateArn, certificatePem, keyPair } =
await iotAdaptor.createKeysAndCertificates();
const { PrivateKey } = keyPair!;
const { policyArn } = await iotAdaptor.createPolicy(thingName);
console.info(`Policy created with ARN: ${policyArn}`);
await iotAdaptor.attachPrincipalPolicy({
policyName: thingName,
principal: certificateArn!,
});
console.info('Policy attached to certificate');
await iotAdaptor.attachThingPrincipal({
principal: certificateArn!,
thingName: thingName,
});
console.info('Certificate attached to thing');
return {
certId: certificateId!,
certPem: certificatePem!,
privKey: PrivateKey!,
thingArn: thingArn!,
};
},
delete: async (thingName) => {
const { principals } = await iotAdaptor.listThingPrincipals(thingName);
for await (const certificateArn of principals!) {
await iotAdaptor.detachPrincipalPolicy({
policyName: thingName,
principal: certificateArn,
});
console.info(`Policy detached from certificate for ${thingName}`);
await iotAdaptor.detachThingPrincipal({
principal: certificateArn,
thingName: thingName,
});
console.info(`Certificate detached from thing for ${certificateArn}`);
await iotAdaptor.updateCertificateToInactive(certificateArn);
console.info(`Certificate marked as inactive for ${certificateArn}`);

await iotAdaptor.deleteCertificate(certificateArn);
console.info(`Certificate deleted from thing for ${certificateArn}`);
await iotAdaptor.deleteThing(thingName);
console.info(`Thing deleted with name: ${thingName}`);
}
await iotAdaptor.deletePolicy(thingName);
console.info(`Policy deleted: ${thingName}`);
},
};
};
68 changes: 68 additions & 0 deletions src/lambda/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
CloudFormationCustomResourceSuccessResponse as Success,
CloudFormationCustomResourceFailedResponse as Failure,
CloudFormationCustomResourceEvent as Event,
} from 'aws-lambda';
import { ServiceException } from '@smithy/smithy-client';
import { IoT } from '@aws-sdk/client-iot';
import { iotAdaptor } from './adapters/iot';
import { thingAdaptor } from './adapters/thing';

const thingHandler = thingAdaptor(iotAdaptor(new IoT()));

export const handler = async (event: Event): Promise<Success | Failure> => {
const { RequestType, LogicalResourceId, RequestId, StackId } = event;

try {
const thingName = event.ResourceProperties.ThingName;
if (RequestType === 'Create') {
const { thingArn, certId, certPem, privKey } = await thingHandler.create(thingName);

return {
Status: 'SUCCESS',
PhysicalResourceId: thingArn,
LogicalResourceId,
RequestId,
StackId,
Data: {
thingArn,
certId,
certPem,
privKey,
},
};
} else if (event.RequestType === 'Delete') {
await thingHandler.delete(thingName);

return {
Status: 'SUCCESS',
PhysicalResourceId: event.PhysicalResourceId,
LogicalResourceId,
RequestId,
StackId,
};
} else if (event.RequestType === 'Update') {
return {
Status: 'SUCCESS',
PhysicalResourceId: event.PhysicalResourceId,
LogicalResourceId,
RequestId,
StackId,
};
} else {
throw new Error('Received invalid request type');
}
} catch (err) {
const Reason = (err as ServiceException).message;

return {
Status: 'FAILED',
Reason,
RequestId,
StackId,
LogicalResourceId,
// @ts-ignore
PhysicalResourceId: event.PhysicalResourceId || LogicalResourceId,
};
}
};
Loading

0 comments on commit e682875

Please sign in to comment.