-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f1f7f11
commit e682875
Showing
12 changed files
with
1,426 additions
and
15 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": [ | ||
"*" | ||
] | ||
} | ||
] | ||
}`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} | ||
}; |
Oops, something went wrong.