From b65a4f07ef28388ea235f9c2761217051d03e6ff Mon Sep 17 00:00:00 2001 From: Stephen Geller Date: Mon, 29 Mar 2021 15:43:44 +0100 Subject: [PATCH] feat: Create GuSSMParameter using AWS Custom Resources (#336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create GuSSMParameter and the AWS Custom Resource that allows it to fetch latest version * Create GuSSMParameter * remove extras * Add tests for random UIDs * update package-lock * feat: revise custom lint rule (#352) We have a few too many instances of // eslint-disable-next-line custom-rules/valid-constructors 😅 . It looks like this is mainly because the id field is static (for example the Stage parameter) or generated by the props passed in (for example InstanceType). This PR revises the rules to be: 1. Private constructors don't get linted 2. Must be 1, 2 or 3 parameters 3. First parameter must be called scope 4. First parameter must be of type GuStack 5. If 2 parameters: - The second parameter must be called props - The second parameter must be a custom type 6. If 3 parameters: - The second parameter must be called id - The second parameter must be of type string - The third parameter must be called props - The third parameter must be a custom type 7. Only the third parameter can be optional or have a default value * chore(release): 6.1.0 (#360) * extract id to fn outside constructor * Add tests for random UIDs Co-authored-by: stephengeller Co-authored-by: Akash Askoolum Co-authored-by: theguardian.com continuous integration --- package-lock.json | 102 +++++++++++++++++- package.json | 2 + .../core/custom-resources/interfaces.ts | 5 + .../core/custom-resources/runtime/lambda.js | 4 + .../runtime/lambda.symlink.ts | 1 + .../custom-resources/runtime/lambda.test.ts | 17 +++ .../core/custom-resources/runtime/lambda.ts | 102 ++++++++++++++++++ src/constructs/core/ssm.test.ts | 58 ++++++++++ src/constructs/core/ssm.ts | 94 ++++++++++++++++ 9 files changed, 383 insertions(+), 2 deletions(-) create mode 100644 src/constructs/core/custom-resources/interfaces.ts create mode 100644 src/constructs/core/custom-resources/runtime/lambda.js create mode 120000 src/constructs/core/custom-resources/runtime/lambda.symlink.ts create mode 100644 src/constructs/core/custom-resources/runtime/lambda.test.ts create mode 100644 src/constructs/core/custom-resources/runtime/lambda.ts create mode 100644 src/constructs/core/ssm.test.ts create mode 100644 src/constructs/core/ssm.ts diff --git a/package-lock.json b/package-lock.json index dcd114a666..e26f506a68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2299,6 +2299,12 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/aws-lambda": { + "version": "8.10.73", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.73.tgz", + "integrity": "sha512-P+a6TRQbRnVQOIjWkmw6F23wiJcF+4Uniasbzx7NAXjLQCVGx/Z4VoMfit81/pxlmcXNxAMGuYPugn6CrJLilQ==", + "dev": true + }, "@types/babel__core": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", @@ -2931,6 +2937,29 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "aws-sdk": { + "version": "2.874.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.874.0.tgz", + "integrity": "sha512-YF2LYIfIuywFTzojGwwUwAiq+pgSM3IWRF/uDoJa28xRQSfYD1uMeZLCqqMt+L0/yxe2UJDYKTVPhB9eEqOxEQ==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -3075,6 +3104,11 @@ } } }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3180,6 +3214,16 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -4941,6 +4985,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "exec-sh": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", @@ -6252,6 +6301,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", @@ -6865,8 +6919,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -7549,6 +7602,11 @@ "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", "dev": true }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13403,6 +13461,11 @@ "strict-uri-encode": "^1.0.0" } }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -14138,6 +14201,11 @@ } } }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, "saxes": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", @@ -15932,6 +16000,22 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", @@ -16216,6 +16300,20 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index 2196276635..df61ca6e6f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@guardian/eslint-config-typescript": "^0.5.0", + "@types/aws-lambda": "^8.10.72", "@types/jest": "^26.0.22", "@types/node": "14.14.37", "@typescript-eslint/eslint-plugin": "^4.19.0", @@ -60,6 +61,7 @@ "@aws-cdk/aws-rds": "1.94.1", "@aws-cdk/aws-s3": "1.94.1", "@aws-cdk/core": "1.94.1", + "aws-sdk": "^2.868.0", "read-pkg-up": "^8.0.0" }, "config": { diff --git a/src/constructs/core/custom-resources/interfaces.ts b/src/constructs/core/custom-resources/interfaces.ts new file mode 100644 index 0000000000..09e0db1ed8 --- /dev/null +++ b/src/constructs/core/custom-resources/interfaces.ts @@ -0,0 +1,5 @@ +import type { GetParameterRequest } from "aws-sdk/clients/ssm"; + +export interface CustomResourceGetParameterProps { + apiRequest: GetParameterRequest; +} diff --git a/src/constructs/core/custom-resources/runtime/lambda.js b/src/constructs/core/custom-resources/runtime/lambda.js new file mode 100644 index 0000000000..7f53b84505 --- /dev/null +++ b/src/constructs/core/custom-resources/runtime/lambda.js @@ -0,0 +1,4 @@ +/* +* The reason for this file is to appease the tests in `core/ssm.test.ts`. +* We don't need this to contain anything useful because we're not testing the contents of it within that testing suite. +* */ diff --git a/src/constructs/core/custom-resources/runtime/lambda.symlink.ts b/src/constructs/core/custom-resources/runtime/lambda.symlink.ts new file mode 120000 index 0000000000..71e3e015fe --- /dev/null +++ b/src/constructs/core/custom-resources/runtime/lambda.symlink.ts @@ -0,0 +1 @@ +../runtime/lambda.ts \ No newline at end of file diff --git a/src/constructs/core/custom-resources/runtime/lambda.test.ts b/src/constructs/core/custom-resources/runtime/lambda.test.ts new file mode 100644 index 0000000000..cc134d1f9d --- /dev/null +++ b/src/constructs/core/custom-resources/runtime/lambda.test.ts @@ -0,0 +1,17 @@ +/* + * You can't specify the file extension (eg .ts) of the file to import, + * and lambda.js exists as a dummy file, so we'd always import that over lambda.ts (the actual code to be tested) if + * we imported `lambda` + * lambda.symlink is a symlink to lambda.ts, so we can test it without importing lambda.js + * */ +import { flatten } from "./lambda.symlink"; + +describe("The flatten function", function () { + it("should flatten nested objects into one", function () { + expect(flatten({ foo: { bar: "baz" } })).toEqual({ "foo.bar": "baz" }); + }); + + it("should flatten infinitely nested objects into one", function () { + expect(flatten({ foo: { bar: { baz: { blah: "foo" } } } })).toEqual({ "foo.bar.baz.blah": "foo" }); + }); +}); diff --git a/src/constructs/core/custom-resources/runtime/lambda.ts b/src/constructs/core/custom-resources/runtime/lambda.ts new file mode 100644 index 0000000000..1824c3b19c --- /dev/null +++ b/src/constructs/core/custom-resources/runtime/lambda.ts @@ -0,0 +1,102 @@ +import { request } from "https"; +import { parse } from "url"; +// eslint-disable-next-line import/no-unresolved -- this comes from @types/aws-lambda, but eslint can't seem to read it properly +import type { CloudFormationCustomResourceEvent, Context } from "aws-lambda"; +import SSM from "aws-sdk/clients/ssm"; +import type { CustomResourceGetParameterProps } from "../interfaces"; + +/* eslint-disable -- This function is copied straight from AWS */ +// https://github.com/aws/aws-cdk/blob/95438b56bfdc90e94f969f6998e5b5b680cbd7a8/packages/%40aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts#L16-L29 +export function flatten(object: Record): Record { + return Object.assign( + {}, + ...(function _flatten(child: Record, path: string[] = []): any { + return [].concat( + ...Object.keys(child).map((key) => { + const childKey = Buffer.isBuffer(child[key]) ? child[key].toString() : child[key]; + return typeof childKey === "object" + ? _flatten(childKey, path.concat([key])) + : { [path.concat([key]).join(".")]: childKey }; + }) + ); + })(object) + ) as Record; +} +/* eslint-enable */ + +export async function handler(event: CloudFormationCustomResourceEvent, context: Context): Promise { + try { + console.log(JSON.stringify(event)); + + // Default physical resource id + let physicalResourceId: string; + switch (event.RequestType) { + case "Create": + physicalResourceId = event.LogicalResourceId; + break; + case "Update": + physicalResourceId = event.PhysicalResourceId; + break; + case "Delete": + await respond("SUCCESS", "OK", event.PhysicalResourceId, {}); + return; + } + + let data: Record = {}; + + const getParamsProps = event.ResourceProperties.getParamsProps as string | undefined; + + if (getParamsProps) { + const request = decodeCall(getParamsProps); + const ssmClient = new SSM(); + const response = await ssmClient.getParameter(request.apiRequest).promise(); + console.log("Response:", JSON.stringify(response, null, 4)); + data = { ...flatten((response as unknown) as Record) }; + } + + console.log("data: ", data); + await respond("SUCCESS", "OK", physicalResourceId, data); + } catch (e) { + console.log(e); + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- e.message is valid, see docs above + await respond("FAILED", e.message || "Internal Error", context.logStreamName, {}); + } + + function respond(responseStatus: string, reason: string, physicalResourceId: string, data: Record) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason, + PhysicalResourceId: physicalResourceId, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: false, + Data: data, + }); + + console.log("Responding", responseBody); + const parsedUrl = parse(event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length }, + }; + + return new Promise((resolve, reject) => { + try { + const r = request(requestOptions, resolve); + r.on("error", reject); + r.write(responseBody); + r.end(); + } catch (e) { + reject(e); + } + }); + } +} + +function decodeCall(call: string): CustomResourceGetParameterProps { + return JSON.parse(call) as CustomResourceGetParameterProps; +} diff --git a/src/constructs/core/ssm.test.ts b/src/constructs/core/ssm.test.ts new file mode 100644 index 0000000000..d52bd987c8 --- /dev/null +++ b/src/constructs/core/ssm.test.ts @@ -0,0 +1,58 @@ +import "@aws-cdk/assert/jest"; +import { simpleGuStackForTesting } from "../../../test/utils"; +import { GuSSMIdentityParameter, GuSSMParameter, id } from "./ssm"; + +describe("SSM:", () => { + describe("The GuSSMIdentityParameter construct", () => { + it("requires the scope and parameter name", function () { + const stack = simpleGuStackForTesting({ stack: "some-stack" }); + const param = new GuSSMIdentityParameter(stack, { parameter: "some-param", app: "foo" }); + expect(stack).toHaveResourceLike("Custom::GuGetSSMParameter", { + getParamsProps: { + "Fn::Join": ["", ['{"apiRequest":{"Name":"/', { Ref: "Stage" }, '/some-stack/foo/some-param"}}']], + }, + }); + expect(param.getValue()).toMatch(/TOKEN/i); + }); + }); + + describe("The GuSSMParameter construct", () => { + it("requires the scope and parameter name", function () { + const stack = simpleGuStackForTesting({ stack: "some-stack" }); + const param = new GuSSMParameter(stack, { parameter: "/path/to/some-param" }); + expect(stack).toHaveResourceLike("Custom::GuGetSSMParameter", { + getParamsProps: '{"apiRequest":{"Name":"/path/to/some-param"}}', + }); + expect(param.getValue()).toMatch(/TOKEN/i); + }); + + it("creates unique IDs using param and stack name for the parameters", function () { + const stack = simpleGuStackForTesting({ stack: "some-stack" }); + const param1 = new GuSSMParameter(stack, { parameter: "/path/to/some-param" }); + const param2 = new GuSSMParameter(stack, { parameter: "/path/to/some-param" }); + + expect(param1.toString()).toMatch(/Test\/GuSSMParameter-pathtosomeparam/i); + expect(param2.toString()).toMatch(/Test\/GuSSMParameter-pathtosomeparam/i); + expect(param1.toString()).not.toEqual(param2.toString()); + }); + + it("creates unique IDs that handles tokens", function () { + const stack = simpleGuStackForTesting({ stack: "some-stack" }); + const param1 = new GuSSMParameter(stack, { parameter: `/path/${stack.stage}/some-param` }); + const param2 = new GuSSMParameter(stack, { parameter: `/path/${stack.stage}/some-param` }); + expect(param1.toString()).toMatch(/Test\/GuSSMParameter-token/i); + expect(param2.toString()).toMatch(/Test\/GuSSMParameter-token/i); + expect(param1.toString()).not.toEqual(param2.toString()); + }); + }); + + describe("the id function", function () { + it("creates a unique ID given a string", function () { + expect(id("NameOfConstruct", "some-parameter")).toMatch(/NameOfConstruct-someparameter/i); + }); + + it("will substitute a CDK token for an acceptable string", function () { + expect(id("NameOfConstruct", "${TOKEN}foobar")).toMatch(/NameOfConstruct-token/i); + }); + }); +}); diff --git a/src/constructs/core/ssm.ts b/src/constructs/core/ssm.ts new file mode 100644 index 0000000000..388d533d06 --- /dev/null +++ b/src/constructs/core/ssm.ts @@ -0,0 +1,94 @@ +import { readFileSync } from "fs"; +import { join } from "path"; +import type { IGrantable, IPrincipal } from "@aws-cdk/aws-iam"; +import { Policy, PolicyStatement } from "@aws-cdk/aws-iam"; +import { Code, Runtime, SingletonFunction } from "@aws-cdk/aws-lambda"; +import type { Reference } from "@aws-cdk/core"; +import { Construct, CustomResource, Duration } from "@aws-cdk/core"; +import { AwsCustomResourcePolicy } from "@aws-cdk/custom-resources"; +import type { CustomResourceGetParameterProps } from "./custom-resources/interfaces"; +import type { AppIdentity } from "./identity"; +import type { GuStack } from "./stack"; + +export interface GuSSMParameterProps { + parameter: string; + secure?: boolean; +} + +export interface GuSSMIdentityParameterProps extends GuSSMParameterProps, AppIdentity {} + +const stripped = (str: string) => str.replace(/[-/]/g, ""); + +export function id(id: string, parameter: string): string { + const now = Date.now(); + // We need to create UIDs for the resources in this construct, as otherwise CFN will not trigger the lambda on updates for resources that appear to be the same + const uid = now.toString().substr(now.toString().length - 4); + return parameter.toUpperCase().includes("TOKEN") ? `${id}-token-${uid}` : `${id}-${stripped(parameter)}-${uid}`; +} + +export class GuSSMParameter extends Construct implements IGrantable { + private readonly customResource: CustomResource; + readonly grantPrincipal: IPrincipal; + + constructor(scope: GuStack, props: GuSSMParameterProps) { + super(scope, id("GuSSMParameter", props.parameter)); + const { parameter } = props; + + const provider = new SingletonFunction(scope, id("Provider", parameter), { + code: Code.fromInline(readFileSync(join(__dirname, "/custom-resources/runtime/lambda.js")).toString()), + runtime: Runtime.NODEJS_12_X, + handler: "index.handler", + uuid: "eda001a3-b7c8-469d-bc13-787c4e13cfd9", + lambdaPurpose: "Lambda to fetch SSM parameters", + timeout: Duration.minutes(2), + }); + + this.grantPrincipal = provider.grantPrincipal; + + const policy = new Policy(scope, id("CustomResourcePolicy", parameter), { + statements: [ + new PolicyStatement({ + actions: ["ssm:getParameter"], + resources: AwsCustomResourcePolicy.ANY_RESOURCE, // This feels too permissive, but possibly okay + }), + ], + }); + + if (provider.role !== undefined) { + policy.attachToRole(provider.role); + } + + const getParamsProps: CustomResourceGetParameterProps = { + apiRequest: { Name: parameter, WithDecryption: props.secure }, + }; + + this.customResource = new CustomResource(this, id("Resource", parameter), { + resourceType: "Custom::GuGetSSMParameter", + serviceToken: provider.functionArn, + pascalCaseProperties: false, + properties: { getParamsProps: JSON.stringify(getParamsProps) }, + }); + + // If the policy was deleted first, then the function might lose permissions to delete the custom resource + // This is here so that the policy doesn't get removed before onDelete is called + this.customResource.node.addDependency(policy); + } + + public getValueReference(): Reference { + return this.customResource.getAtt("Parameter.Value"); + } + + public getValue(): string { + return this.customResource.getAttString("Parameter.Value"); + } +} + +/* + * Assumes the path of `/${STAGE}/${STACK}/${APP}/${parameter}` + * + * */ +export class GuSSMIdentityParameter extends GuSSMParameter { + constructor(scope: GuStack, props: GuSSMIdentityParameterProps) { + super(scope, { ...props, parameter: `/${scope.stage}/${scope.stack}/${props.app}/${props.parameter}` }); + } +}