diff --git a/.vscode/settings.json b/.vscode/settings.json index f6d8629..407ddaf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,29 +35,33 @@ "jest.runMode": "on-demand", "editor.tabSize": 2, "explorer.excludeGitIgnore": false, - "files.exclude": { - "**/.git": false, - "**/.gitignore": false, - "node_modules": true, - "dist": true, - ".cache": true - }, + // "files.exclude": { + // "**/.git": false, + // "**/.gitignore": false, + // "node_modules": true, + // "dist": true, + // ".cache": true + // }, "files.associations": { "*.test.ts": "typescript" }, "typescript.suggest.autoImports": true, "typescript.preferences.importModuleSpecifier": "relative", - "typescript.referencesCodeLens.enabled": true, + "typescript.referencesCodeLens.enabled": false, "typescript.updateImportsOnFileMove.enabled": "always", "cSpell.words": [ "AAAAWWW", + "accountid", "apigatewayv", "apigwi", "Brotli", "fieldname", + "idmg", "kqxq", "lambdajs", + "logarchive", "mebibytes", + "mgmt", "mybucketf", "ngfg", "presign", diff --git a/bin/deploy.ts b/bin/deploy.ts index d6cc431..5fd925c 100644 --- a/bin/deploy.ts +++ b/bin/deploy.ts @@ -2,7 +2,7 @@ import process from 'node:process' import * as cdk from 'aws-cdk-lib' -import { AccountVendingMachineStack } from '../src/account-vending-machine' +import { AccountVendingMachineStack } from '../src/avm' import { OrganizationStack } from '../src/organization' const env = { @@ -12,5 +12,5 @@ const env = { const app = new cdk.App() new OrganizationStack(app, 'p6-lz-organization', { env }) -new AccountVendingMachineStack(app, 'p6-lz-account-vending-machine', { env }) +new AccountVendingMachineStack(app, 'p6-lz-avm', { env }) app.synth() diff --git a/conf/accounts.yml b/conf/accounts.yml new file mode 100644 index 0000000..ce58c32 --- /dev/null +++ b/conf/accounts.yml @@ -0,0 +1,12 @@ +- Name: p6m7g8-audit + Email: pgollucci-aws-p6m7g8-audits@p6m7g8.com + ou: Security +- Name: p6m7g8-logarchive + Email: pgollucci-aws-p6m7g8-logarchive@p6m7g8.com + ou: Security +- Name: p6m7g8-shared + Email: pgollucci-aws-p6m7g8-shared@p6m7g8.com + ou: Infrastructure +- Name: p6m7g8-prod + Email: pgollucci-p6m7g8-prod@p6m7g8.com + ou: Production diff --git a/conf/ou.yml b/conf/ou.yml new file mode 100644 index 0000000..6bd199b --- /dev/null +++ b/conf/ou.yml @@ -0,0 +1,4 @@ +- Name: Infrastructure +- Name: Production +- Name: Security +- Name: Suspended diff --git a/package.json b/package.json index 01a1101..11461f2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@antfu/eslint-config": "^3.8.0", "@types/aws-lambda": "^8.10.145", "@types/jest": "^29.5.14", + "@types/js-yaml": "^4.0.9", "@types/node": "22.8.6", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", @@ -31,6 +32,7 @@ "dependencies": { "@aws-sdk/client-dynamodb": "^3.687.0", "@aws-sdk/client-organizations": "^3.687.0", + "@aws-sdk/client-s3": "^3.688.0", "@aws-sdk/lib-dynamodb": "^3.687.0", "aws-cdk-lib": "2.165.0", "cdk-iam-floyd": "^0.658.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a2808c..ab2dfb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@aws-sdk/client-organizations': specifier: ^3.687.0 version: 3.687.0 + '@aws-sdk/client-s3': + specifier: ^3.688.0 + version: 3.688.0 '@aws-sdk/lib-dynamodb': specifier: ^3.687.0 version: 3.687.0(@aws-sdk/client-dynamodb@3.687.0) @@ -42,6 +45,9 @@ importers: '@types/jest': specifier: ^29.5.14 version: 29.5.14 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/node': specifier: 22.8.6 version: 22.8.6 @@ -149,6 +155,16 @@ packages: - jsonschema - semver + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -170,6 +186,10 @@ packages: resolution: {integrity: sha512-Y2bGWcB+RAJWzsTTMjhRhp+yQ5w03hZQ+z/jdExf0aw9HEB1RMrts94EN0p1KyhflSy6tKjzwSTRWWa3L5tDOg==} engines: {node: '>=16.0.0'} + '@aws-sdk/client-s3@3.688.0': + resolution: {integrity: sha512-bLyF7gT0RTWrsJPxbaslg1xP1gUdw3BJVvgfWM/63BDBpVCqIk9YlrXfJwjImcKguxGp8sCTdttywmfdPwQEfg==} + engines: {node: '>=16.0.0'} + '@aws-sdk/client-sso-oidc@3.687.0': resolution: {integrity: sha512-Rdd8kLeTeh+L5ZuG4WQnWgYgdv7NorytKdZsGjiag1D8Wv3PcJvPqqWdgnI0Og717BSXVoaTYaN34FyqFYSx6Q==} engines: {node: '>=16.0.0'} @@ -230,14 +250,30 @@ packages: peerDependencies: '@aws-sdk/client-dynamodb': ^3.687.0 + '@aws-sdk/middleware-bucket-endpoint@3.686.0': + resolution: {integrity: sha512-6qCoWI73/HDzQE745MHQUYz46cAQxHCgy1You8MZQX9vHAQwqBnkcsb2hGp7S6fnQY5bNsiZkMWVQ/LVd2MNjg==} + engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-endpoint-discovery@3.686.0': resolution: {integrity: sha512-4A+VmWf3vUirzncM0reyG/J3m82mDv2UbmCBz+RcYQ6S41JCC2WxN/MD2oIN/Qkd1N+4OW2U+T62VmqFQgeBKg==} engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-expect-continue@3.686.0': + resolution: {integrity: sha512-5yYqIbyhLhH29vn4sHiTj7sU6GttvLMk3XwCmBXjo2k2j3zHqFUwh9RyFGF9VY6Z392Drf/E/cl+qOGypwULpg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.688.0': + resolution: {integrity: sha512-diIBWLpM5eg3sRggKSKGUJGBh8VyFo/wZLq80GSq1kxGlmJOMvwT6YvE+Z51xhEbYTKIjX9IH/NhO7W4pA3MNw==} + engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-host-header@3.686.0': resolution: {integrity: sha512-+Yc6rO02z+yhFbHmRZGvEw1vmzf/ifS9a4aBjJGeVVU+ZxaUvnk+IUZWrj4YQopUQ+bSujmMUzJLXSkbDq7yuw==} engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-location-constraint@3.686.0': + resolution: {integrity: sha512-pCLeZzt5zUGY3NbW4J/5x3kaHyJEji4yqtoQcUlJmkoEInhSxJ0OE8sTxAfyL3nIOF4yr6L2xdaLCqYgQT8Aog==} + engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-logger@3.686.0': resolution: {integrity: sha512-cX43ODfA2+SPdX7VRxu6gXk4t4bdVJ9pkktbfnkE5t27OlwNfvSGGhnHrQL8xTOFeyQ+3T+oowf26gf1OI+vIg==} engines: {node: '>=16.0.0'} @@ -246,6 +282,14 @@ packages: resolution: {integrity: sha512-jF9hQ162xLgp9zZ/3w5RUNhmwVnXDBlABEUX8jCgzaFpaa742qR/KKtjjZQ6jMbQnP+8fOCSXFAVNMU+s6v81w==} engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-sdk-s3@3.687.0': + resolution: {integrity: sha512-YGHYqiyRiNNucmvLrfx3QxIkjSDWR/+cc72bn0lPvqFUQBRHZgmYQLxVYrVZSmRzzkH2FQ1HsZcXhOafLbq4vQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-ssec@3.686.0': + resolution: {integrity: sha512-zJXml/CpVHFUdlGQqja87vNQ3rPB5SlDbfdwxlj1KBbjnRRwpBtxxmOlWRShg8lnVV6aIMGv95QmpIFy4ayqnQ==} + engines: {node: '>=16.0.0'} + '@aws-sdk/middleware-user-agent@3.687.0': resolution: {integrity: sha512-nUgsKiEinyA50CaDXojAkOasAU3Apdg7Qox6IjNUC4ZjgOu7QWsCDB5N28AYMUt06cNYeYQdfMX1aEzG85a1Mg==} engines: {node: '>=16.0.0'} @@ -254,6 +298,10 @@ packages: resolution: {integrity: sha512-6zXD3bSD8tcsMAVVwO1gO7rI1uy2fCD3czgawuPGPopeLiPpo6/3FoUWCQzk2nvEhj7p9Z4BbjwZGSlRkVrXTw==} engines: {node: '>=16.0.0'} + '@aws-sdk/signature-v4-multi-region@3.687.0': + resolution: {integrity: sha512-vdOQHCRHJPX9mT8BM6xOseazHD6NodvHl9cyF5UjNtLn+gERRJEItIA9hf0hlt62odGD8Fqp+rFRuqdmbNkcNw==} + engines: {node: '>=16.0.0'} + '@aws-sdk/token-providers@3.686.0': resolution: {integrity: sha512-9oL4kTCSePFmyKPskibeiOXV6qavPZ63/kXM9Wh9V6dTSvBtLeNnMxqGvENGKJcTdIgtoqyqA6ET9u0PJ5IRIg==} engines: {node: '>=16.0.0'} @@ -264,6 +312,10 @@ packages: resolution: {integrity: sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==} engines: {node: '>=16.0.0'} + '@aws-sdk/util-arn-parser@3.679.0': + resolution: {integrity: sha512-CwzEbU8R8rq9bqUFryO50RFBlkfufV9UfMArHPWlo+lmsC+NlSluHQALoj6Jkq3zf5ppn1CN0c1DDLrEqdQUXg==} + engines: {node: '>=16.0.0'} + '@aws-sdk/util-dynamodb@3.687.0': resolution: {integrity: sha512-0Xg/34GgRcmD7PxHD5LPpxd+h8qpdM6A9iPpz4UIokkQo3Baa+BrBBa6h7tgQwrvfWm0oX63Pa5U3ZKY1yHxQw==} engines: {node: '>=16.0.0'} @@ -290,6 +342,10 @@ packages: aws-crt: optional: true + '@aws-sdk/xml-builder@3.686.0': + resolution: {integrity: sha512-k0z5b5dkYSuOHY0AOZ4iyjcGBeVL9lWsQNF4+c+1oK3OW4fRWl/bNa1soMRMpangsHPzgyn/QkzuDbl7qR4qrw==} + engines: {node: '>=16.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -822,6 +878,12 @@ packages: resolution: {integrity: sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==} engines: {node: '>=16.0.0'} + '@smithy/chunked-blob-reader-native@3.0.1': + resolution: {integrity: sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==} + + '@smithy/chunked-blob-reader@4.0.0': + resolution: {integrity: sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==} + '@smithy/config-resolver@3.0.10': resolution: {integrity: sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw==} engines: {node: '>=16.0.0'} @@ -834,13 +896,39 @@ packages: resolution: {integrity: sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==} engines: {node: '>=16.0.0'} + '@smithy/eventstream-codec@3.1.7': + resolution: {integrity: sha512-kVSXScIiRN7q+s1x7BrQtZ1Aa9hvvP9FeCqCdBxv37GimIHgBCOnZ5Ip80HLt0DhnAKpiobFdGqTFgbaJNrazA==} + + '@smithy/eventstream-serde-browser@3.0.11': + resolution: {integrity: sha512-Pd1Wnq3CQ/v2SxRifDUihvpXzirJYbbtXfEnnLV/z0OGCTx/btVX74P86IgrZkjOydOASBGXdPpupYQI+iO/6A==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-config-resolver@3.0.8': + resolution: {integrity: sha512-zkFIG2i1BLbfoGQnf1qEeMqX0h5qAznzaZmMVNnvPZz9J5AWBPkOMckZWPedGUPcVITacwIdQXoPcdIQq5FRcg==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-node@3.0.10': + resolution: {integrity: sha512-hjpU1tIsJ9qpcoZq9zGHBJPBOeBGYt+n8vfhDwnITPhEre6APrvqq/y3XMDEGUT2cWQ4ramNqBPRbx3qn55rhw==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-universal@3.0.10': + resolution: {integrity: sha512-ewG1GHbbqsFZ4asaq40KmxCmXO+AFSM1b+DcO2C03dyJj/ZH71CiTg853FSE/3SHK9q3jiYQIFjlGSwfxQ9kww==} + engines: {node: '>=16.0.0'} + '@smithy/fetch-http-handler@4.0.0': resolution: {integrity: sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==} + '@smithy/hash-blob-browser@3.1.7': + resolution: {integrity: sha512-4yNlxVNJifPM5ThaA5HKnHkn7JhctFUHvcaz6YXxHlYOSIrzI6VKQPTN8Gs1iN5nqq9iFcwIR9THqchUCouIfg==} + '@smithy/hash-node@3.0.8': resolution: {integrity: sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng==} engines: {node: '>=16.0.0'} + '@smithy/hash-stream-node@3.1.7': + resolution: {integrity: sha512-xMAsvJ3hLG63lsBVi1Hl6BBSfhd8/Qnp8fC06kjOpJvyyCEXdwHITa5Kvdsk6gaAXLhbZMhQMIGvgUbfnJDP6Q==} + engines: {node: '>=16.0.0'} + '@smithy/invalid-dependency@3.0.8': resolution: {integrity: sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q==} @@ -852,6 +940,9 @@ packages: resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} engines: {node: '>=16.0.0'} + '@smithy/md5-js@3.0.8': + resolution: {integrity: sha512-LwApfTK0OJ/tCyNUXqnWCKoE2b4rDSr4BJlDAVCkiWYeHESr+y+d5zlAanuLW6fnitVJRD/7d9/kN/ZM9Su4mA==} + '@smithy/middleware-content-length@3.0.10': resolution: {integrity: sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg==} engines: {node: '>=16.0.0'} @@ -1115,6 +1206,9 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3473,6 +3567,27 @@ snapshots: '@aws-cdk/cloud-assembly-schema@38.0.1': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.686.0 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.686.0 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.686.0 + '@aws-sdk/util-locate-window': 3.679.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + '@aws-crypto/sha256-browser@5.2.0': dependencies: '@aws-crypto/sha256-js': 5.2.0 @@ -3595,6 +3710,69 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-s3@3.688.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.687.0(@aws-sdk/client-sts@3.687.0) + '@aws-sdk/client-sts': 3.687.0 + '@aws-sdk/core': 3.686.0 + '@aws-sdk/credential-provider-node': 3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0))(@aws-sdk/client-sts@3.687.0) + '@aws-sdk/middleware-bucket-endpoint': 3.686.0 + '@aws-sdk/middleware-expect-continue': 3.686.0 + '@aws-sdk/middleware-flexible-checksums': 3.688.0 + '@aws-sdk/middleware-host-header': 3.686.0 + '@aws-sdk/middleware-location-constraint': 3.686.0 + '@aws-sdk/middleware-logger': 3.686.0 + '@aws-sdk/middleware-recursion-detection': 3.686.0 + '@aws-sdk/middleware-sdk-s3': 3.687.0 + '@aws-sdk/middleware-ssec': 3.686.0 + '@aws-sdk/middleware-user-agent': 3.687.0 + '@aws-sdk/region-config-resolver': 3.686.0 + '@aws-sdk/signature-v4-multi-region': 3.687.0 + '@aws-sdk/types': 3.686.0 + '@aws-sdk/util-endpoints': 3.686.0 + '@aws-sdk/util-user-agent-browser': 3.686.0 + '@aws-sdk/util-user-agent-node': 3.687.0 + '@aws-sdk/xml-builder': 3.686.0 + '@smithy/config-resolver': 3.0.10 + '@smithy/core': 2.5.1 + '@smithy/eventstream-serde-browser': 3.0.11 + '@smithy/eventstream-serde-config-resolver': 3.0.8 + '@smithy/eventstream-serde-node': 3.0.10 + '@smithy/fetch-http-handler': 4.0.0 + '@smithy/hash-blob-browser': 3.1.7 + '@smithy/hash-node': 3.0.8 + '@smithy/hash-stream-node': 3.1.7 + '@smithy/invalid-dependency': 3.0.8 + '@smithy/md5-js': 3.0.8 + '@smithy/middleware-content-length': 3.0.10 + '@smithy/middleware-endpoint': 3.2.1 + '@smithy/middleware-retry': 3.0.25 + '@smithy/middleware-serde': 3.0.8 + '@smithy/middleware-stack': 3.0.8 + '@smithy/node-config-provider': 3.1.9 + '@smithy/node-http-handler': 3.2.5 + '@smithy/protocol-http': 4.1.5 + '@smithy/smithy-client': 3.4.2 + '@smithy/types': 3.6.0 + '@smithy/url-parser': 3.0.8 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.25 + '@smithy/util-defaults-mode-node': 3.0.25 + '@smithy/util-endpoints': 2.1.4 + '@smithy/util-middleware': 3.0.8 + '@smithy/util-retry': 3.0.8 + '@smithy/util-stream': 3.2.1 + '@smithy/util-utf8': 3.0.0 + '@smithy/util-waiter': 3.1.7 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -3848,6 +4026,16 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/middleware-bucket-endpoint@3.686.0': + dependencies: + '@aws-sdk/types': 3.686.0 + '@aws-sdk/util-arn-parser': 3.679.0 + '@smithy/node-config-provider': 3.1.9 + '@smithy/protocol-http': 4.1.5 + '@smithy/types': 3.6.0 + '@smithy/util-config-provider': 3.0.0 + tslib: 2.8.1 + '@aws-sdk/middleware-endpoint-discovery@3.686.0': dependencies: '@aws-sdk/endpoint-cache': 3.679.0 @@ -3857,6 +4045,29 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/middleware-expect-continue@3.686.0': + dependencies: + '@aws-sdk/types': 3.686.0 + '@smithy/protocol-http': 4.1.5 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.688.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.686.0 + '@aws-sdk/types': 3.686.0 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/node-config-provider': 3.1.9 + '@smithy/protocol-http': 4.1.5 + '@smithy/types': 3.6.0 + '@smithy/util-middleware': 3.0.8 + '@smithy/util-stream': 3.2.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + '@aws-sdk/middleware-host-header@3.686.0': dependencies: '@aws-sdk/types': 3.686.0 @@ -3864,6 +4075,12 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/middleware-location-constraint@3.686.0': + dependencies: + '@aws-sdk/types': 3.686.0 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + '@aws-sdk/middleware-logger@3.686.0': dependencies: '@aws-sdk/types': 3.686.0 @@ -3877,6 +4094,29 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/middleware-sdk-s3@3.687.0': + dependencies: + '@aws-sdk/core': 3.686.0 + '@aws-sdk/types': 3.686.0 + '@aws-sdk/util-arn-parser': 3.679.0 + '@smithy/core': 2.5.1 + '@smithy/node-config-provider': 3.1.9 + '@smithy/protocol-http': 4.1.5 + '@smithy/signature-v4': 4.2.1 + '@smithy/smithy-client': 3.4.2 + '@smithy/types': 3.6.0 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.8 + '@smithy/util-stream': 3.2.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.686.0': + dependencies: + '@aws-sdk/types': 3.686.0 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + '@aws-sdk/middleware-user-agent@3.687.0': dependencies: '@aws-sdk/core': 3.686.0 @@ -3896,6 +4136,15 @@ snapshots: '@smithy/util-middleware': 3.0.8 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.687.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.687.0 + '@aws-sdk/types': 3.686.0 + '@smithy/protocol-http': 4.1.5 + '@smithy/signature-v4': 4.2.1 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.686.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0))': dependencies: '@aws-sdk/client-sso-oidc': 3.687.0(@aws-sdk/client-sts@3.687.0) @@ -3910,6 +4159,10 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/util-arn-parser@3.679.0': + dependencies: + tslib: 2.8.1 + '@aws-sdk/util-dynamodb@3.687.0(@aws-sdk/client-dynamodb@3.687.0)': dependencies: '@aws-sdk/client-dynamodb': 3.687.0 @@ -3941,6 +4194,11 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.686.0': + dependencies: + '@smithy/types': 3.6.0 + tslib: 2.8.1 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -4531,6 +4789,15 @@ snapshots: '@smithy/types': 3.6.0 tslib: 2.8.1 + '@smithy/chunked-blob-reader-native@3.0.1': + dependencies: + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@4.0.0': + dependencies: + tslib: 2.8.1 + '@smithy/config-resolver@3.0.10': dependencies: '@smithy/node-config-provider': 3.1.9 @@ -4558,6 +4825,36 @@ snapshots: '@smithy/url-parser': 3.0.8 tslib: 2.8.1 + '@smithy/eventstream-codec@3.1.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 3.6.0 + '@smithy/util-hex-encoding': 3.0.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@3.0.11': + dependencies: + '@smithy/eventstream-serde-universal': 3.0.10 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@3.0.8': + dependencies: + '@smithy/types': 3.6.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@3.0.10': + dependencies: + '@smithy/eventstream-serde-universal': 3.0.10 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@3.0.10': + dependencies: + '@smithy/eventstream-codec': 3.1.7 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + '@smithy/fetch-http-handler@4.0.0': dependencies: '@smithy/protocol-http': 4.1.5 @@ -4566,6 +4863,13 @@ snapshots: '@smithy/util-base64': 3.0.0 tslib: 2.8.1 + '@smithy/hash-blob-browser@3.1.7': + dependencies: + '@smithy/chunked-blob-reader': 4.0.0 + '@smithy/chunked-blob-reader-native': 3.0.1 + '@smithy/types': 3.6.0 + tslib: 2.8.1 + '@smithy/hash-node@3.0.8': dependencies: '@smithy/types': 3.6.0 @@ -4573,6 +4877,12 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 + '@smithy/hash-stream-node@3.1.7': + dependencies: + '@smithy/types': 3.6.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + '@smithy/invalid-dependency@3.0.8': dependencies: '@smithy/types': 3.6.0 @@ -4586,6 +4896,12 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/md5-js@3.0.8': + dependencies: + '@smithy/types': 3.6.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + '@smithy/middleware-content-length@3.0.10': dependencies: '@smithy/protocol-http': 4.1.5 @@ -4922,6 +5238,8 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} diff --git a/src/account-vending-machine.AccountVendingMachine.ts b/src/account-vending-machine.AccountVendingMachine.ts deleted file mode 100644 index 9a14022..0000000 --- a/src/account-vending-machine.AccountVendingMachine.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { Context, ScheduledEvent } from 'aws-lambda' -import type { Logger } from 'winston' -import * as process from 'node:process' -import { DynamoDBClient } from '@aws-sdk/client-dynamodb' -import { CreateAccountCommand, DescribeCreateAccountStatusCommand, OrganizationsClient } from '@aws-sdk/client-organizations' -import { DynamoDBDocumentClient, ScanCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb' -import winston from 'winston' - -// Configure the logger -const logger: Logger = winston.createLogger({ - level: 'info', - format: winston.format.json(), - defaultMeta: { service: 'user-service' }, - transports: [ - new winston.transports.Console(), - ], -}) - -const org = new OrganizationsClient({}) -const ddbClient = new DynamoDBClient({}) -const docClient = DynamoDBDocumentClient.from(ddbClient) -const tableName = process.env.TABLE_NAME - -async function scanDynamoDB() { - const scanCommand = new ScanCommand({ - TableName: tableName, - FilterExpression: 'attribute_not_exists(#status) OR #status <> :status', - ExpressionAttributeNames: { - '#status': 'status', - }, - ExpressionAttributeValues: { - ':status': 'SUCCESS', - }, - }) - - return docClient.send(scanCommand) -} - -async function processItem(item: any, requestId: string) { - logger.info('Processing item', { item }) - - try { - const response = await org.send( - new CreateAccountCommand({ - Email: item.email, - AccountName: item.name, - IamUserAccessToBilling: 'ALLOW', - }), - ) - logger.info('CreateAccountCommand response', { response }) - - const createAccountRequestId = response.CreateAccountStatus?.Id - if (!createAccountRequestId) { - throw new Error('CreateAccountStatus Id not found.') - } - - const isSuccess = await pollAccountCreationStatus(createAccountRequestId) - if (isSuccess) { - await updateItemStatus(item.name) - } - else { - logger.error('Account creation failed', { item }) - } - } - catch (error) { - logger.error('Error creating account', { requestId, item, error }) - } -} - -async function pollAccountCreationStatus(createAccountRequestId: string): Promise { - while (true) { - const describeResponse = await org.send( - new DescribeCreateAccountStatusCommand({ - CreateAccountRequestId: createAccountRequestId, - }), - ) - logger.info('DescribeCreateAccountStatus response', { describeResponse }) - - const status = describeResponse.CreateAccountStatus?.State - logger.info(`status: [${status}]`) - - if (status === 'SUCCEEDED') { - return true - } - else if (status === 'FAILED') { - return false - } - - await new Promise(resolve => setTimeout(resolve, 5000)) - } -} - -async function updateItemStatus(itemName: string) { - const updateCommand = new UpdateCommand({ - TableName: tableName, - Key: { name: itemName }, - UpdateExpression: 'SET #status = :status', - ExpressionAttributeNames: { - '#status': 'status', - }, - ExpressionAttributeValues: { - ':status': 'SUCCESS', - }, - }) - - await docClient.send(updateCommand) -} - -export async function handler(event: ScheduledEvent, context: Context): Promise { - logger.info('Scheduled Event Triggered', { requestId: context.awsRequestId, timestamp: event.time }) - - try { - const result = await scanDynamoDB() - - if (result.Count && result.Items) { - logger.info('Items found in DynamoDB table', { requestId: context.awsRequestId, items: result.Items }) - - for (const item of result.Items) { - await processItem(item, context.awsRequestId) - } - } - else { - logger.info('No items found in DynamoDB table', { requestId: context.awsRequestId }) - } - } - catch (error) { - logger.error('Error scanning DynamoDB table', { requestId: context.awsRequestId, error }) - return false - } - - return true -} diff --git a/src/account-vending-machine.OrganizationUnits.ts b/src/account-vending-machine.OrganizationUnits.ts deleted file mode 100644 index 3622e18..0000000 --- a/src/account-vending-machine.OrganizationUnits.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Context, ScheduledEvent } from 'aws-lambda' -import type { Logger } from 'winston' -import * as process from 'node:process' -import { DynamoDBClient } from '@aws-sdk/client-dynamodb' -import { CreateOrganizationalUnitCommand, ListOrganizationalUnitsForParentCommand, OrganizationsClient } from '@aws-sdk/client-organizations' -import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb' -import winston from 'winston' - -// Configure the logger -const logger: Logger = winston.createLogger({ - level: 'info', - format: winston.format.json(), - defaultMeta: { service: 'ou-structuring-service' }, - transports: [ - new winston.transports.Console(), - ], -}) - -const ddbClient = new DynamoDBClient({}) -const docClient = DynamoDBDocumentClient.from(ddbClient) -const orgClient = new OrganizationsClient({}) -const TABLE_NAME = process.env.TABLE_NAME -const ROOT_OU_ID = process.env.ROOT_OU_ID - -export async function handler(event: ScheduledEvent, context: Context): Promise { - logger.info('Scheduled Event Triggered', { requestId: context.awsRequestId, timestamp: event.time }) - - try { - const ouStructure = await getOrganizationUnitStructure() - logger.info('Structured Organization Units', { ouStructure }) - - await createOUsRecursively(ouStructure, ROOT_OU_ID!) - } - catch (error) { - logger.error('Error structuring Organization Units', { requestId: context.awsRequestId, error }) - return false - } - - return true -} - -async function getOrganizationUnitStructure(): Promise { - const scanCommand = new ScanCommand({ TableName: TABLE_NAME }) - const result = await docClient.send(scanCommand) - - if (!result.Items || result.Count === 0) { - throw new Error('No items found in OrganizationUnits table') - } - - const ouMap: Record = {} - const rootOUs: any[] = [] - - result.Items.forEach((item) => { - ouMap[item.id] = { ...item, children: [] } - }) - - result.Items.forEach((item) => { - if (item.parent && ouMap[item.parent]) { - ouMap[item.parent].children.push(ouMap[item.id]) - } - else { - rootOUs.push(ouMap[item.id]) - } - }) - - return rootOUs -} - -async function createOUsRecursively(ous: any[], parentId: string) { - for (const ou of ous) { - const existingOuId = await getExistingOuId(parentId, ou.name) - - const ouId = existingOuId ?? await createOrganizationalUnit(parentId, ou.name) - if (ou.children && ou.children.length > 0) { - await createOUsRecursively(ou.children, ouId) - } - } -} - -async function getExistingOuId(parentId: string, ouName: string): Promise { - const listCommand = new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }) - const result = await orgClient.send(listCommand) - const existingOu = result.OrganizationalUnits?.find(ou => ou.Name === ouName) - return existingOu?.Id ?? null -} - -async function createOrganizationalUnit(parentId: string, ouName: string): Promise { - logger.info('Creating Organizational Unit', { ouName, parentId }) - const createCommand = new CreateOrganizationalUnitCommand({ - ParentId: parentId, - Name: ouName, - }) - const response = await orgClient.send(createCommand) - logger.info('Organizational Unit Created', { ouName, ouId: response.OrganizationalUnit?.Id }) - return response.OrganizationalUnit!.Id! -} diff --git a/src/account-vending-machine.ts b/src/account-vending-machine.ts deleted file mode 100644 index 12f766e..0000000 --- a/src/account-vending-machine.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Construct } from 'constructs' -import * as cdk from 'aws-cdk-lib' -import * as dynamodb from 'aws-cdk-lib/aws-dynamodb' -import * as events from 'aws-cdk-lib/aws-events' -import * as targets from 'aws-cdk-lib/aws-events-targets' -import * as lambda from 'aws-cdk-lib/aws-lambda' -import * as lambdajs from 'aws-cdk-lib/aws-lambda-nodejs' -import * as floyd from 'cdk-iam-floyd' - -export class AccountVendingMachineStack extends cdk.Stack { - constructor(scope: Construct, id: string, props: cdk.StackProps) { - super(scope, id, props) - - const avmTable = this.createAccountsTable() - this.createAVMFunction(avmTable) - - const ouTable = this.createOuTable() - this.createOUFunction(ouTable) - } - - private createAccountsTable(): dynamodb.Table { - return new dynamodb.Table(this, 'AccountTable', { - partitionKey: { name: 'name', type: dynamodb.AttributeType.STRING }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - }) - } - - private createOuTable(): dynamodb.Table { - return new dynamodb.Table(this, 'OUTable', { - partitionKey: { name: 'name', type: dynamodb.AttributeType.STRING }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - }) - } - - private createAVMFunction(table: dynamodb.Table): lambda.Function { - const avmLambda = new lambdajs.NodejsFunction(this, 'AccountVendingMachine', { - runtime: lambda.Runtime.NODEJS_20_X, - timeout: cdk.Duration.minutes(3), - tracing: lambda.Tracing.ACTIVE, - bundling: { - minify: true, - }, - environment: { - TABLE_NAME: table.tableName, - }, - }) - table.grantReadWriteData(avmLambda) - - const policy = new floyd.Statement.Organizations().allow().toCreateAccount() - avmLambda.addToRolePolicy(policy) - - new events.Rule(this, 'OUScheduleRule', { - schedule: events.Schedule.rate(cdk.Duration.minutes(15)), - targets: [new targets.LambdaFunction(avmLambda)], - }) - - return avmLambda - } - - private createOUFunction(table: dynamodb.Table): lambda.Function { - const ouLambda = new lambdajs.NodejsFunction(this, 'OrganizationUnits', { - runtime: lambda.Runtime.NODEJS_20_X, - timeout: cdk.Duration.minutes(1), - tracing: lambda.Tracing.ACTIVE, - bundling: { - minify: true, - }, - environment: { - TABLE_NAME: table.tableName, - }, - }) - table.grantReadData(ouLambda) - - const policy = new floyd.Statement.Organizations().allow().toCreateOrganizationalUnit().toListOrganizationalUnitsForParent() - ouLambda.addToRolePolicy(policy) - - new events.Rule(this, 'AccountVendingMachineScheduleRule', { - schedule: events.Schedule.rate(cdk.Duration.minutes(15)), - targets: [new targets.LambdaFunction(ouLambda)], - }) - - return ouLambda - } -} diff --git a/src/avm.ts b/src/avm.ts new file mode 100644 index 0000000..d184cbb --- /dev/null +++ b/src/avm.ts @@ -0,0 +1,57 @@ +import type { Account, OrganizationalUnit } from '@aws-sdk/client-organizations' +import type { Construct } from 'constructs' +import * as fs from 'node:fs' +import * as cdk from 'aws-cdk-lib' +import { CfnAccount, CfnOrganizationalUnit } from 'aws-cdk-lib/aws-organizations' +import * as yaml from 'js-yaml' + +const OU_FILE = 'conf/ou.yml' +const ACCOUNTS_FILE = 'conf/accounts.yml' + +type OrganizationalUnits = OrganizationalUnit[] + +type MyAccount = Account & { ou: string } +type MyAccounts = MyAccount[] + +function parseOUYamlFile(filePath: string): OrganizationalUnits { + const fileContents = fs.readFileSync(filePath, 'utf8') + const yamlData = yaml.load(fileContents) as OrganizationalUnits + return yamlData +} + +function parseAccountsYamlFile(filePath: string): MyAccounts { + const fileContents = fs.readFileSync(filePath, 'utf8') + const yamlData = yaml.load(fileContents) as MyAccounts + return yamlData +} + +export class AccountVendingMachineStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: cdk.StackProps) { + super(scope, id, props) + + const rootOuId = cdk.Fn.importValue('RootOuId') + const organizationalUnits = parseOUYamlFile(OU_FILE) + const ouMap: Record = {} + organizationalUnits.forEach((organizationalUnit: OrganizationalUnit) => { + const ou = new CfnOrganizationalUnit(this, `OU-${organizationalUnit.Name!}`, { + name: organizationalUnit.Name!, + parentId: rootOuId, + }) + ouMap[organizationalUnit.Name!] = ou + }) + + const accounts = parseAccountsYamlFile(ACCOUNTS_FILE) + accounts.forEach((account: MyAccount) => { + const ouName = account.ou + const ou = ouMap[ouName] + if (!ou) { + throw new Error(`Organizational Unit ${ouName} not found`) + } + new CfnAccount(this, `Account-${account.Name!}`, { + accountName: account.Name!, + email: account.Email!, + parentIds: [ou.ref], + }) + }) + } +} diff --git a/src/organization.ts b/src/organization.ts index fc642e1..1b95c80 100644 --- a/src/organization.ts +++ b/src/organization.ts @@ -21,6 +21,7 @@ export class OrganizationStack extends cdk.Stack { value: organizationArn, }) new cdk.CfnOutput(this, 'RootOuId', { + exportName: 'RootOuId', value: rootOuId, }) }