├── .adr-dir ├── .eslintrc.js ├── .gitignore ├── .jshintrc ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── aws-sso-extensions-for-enterprise.ts ├── cdk.json ├── cfn-nag └── cfn_nag_ignored_rules.yaml ├── config ├── env.yaml └── region-switch.yaml ├── docs ├── adr │ └── 0001-record-architecture-decisions.md ├── architecture │ └── decisions │ │ ├── 0001-record-architecture-decisions.md │ │ ├── 0002-continuous-sync-for-permission-sets.md │ │ ├── 0003-continuous-sync-for-account-assignments.md │ │ └── 0004-region-switch-enhancements.md ├── documentation │ ├── Building-Blocks.md │ ├── Region-Switch.md │ ├── Scaling.md │ ├── TroubleShooting.md │ └── Use-Case-Flows.md ├── images │ ├── account-assignment-lifecycle.png │ ├── average-duration.jpg │ ├── aws-sso-extensions-for-enterprise-overview.png │ ├── concurrent-executions.jpg │ ├── current-aws-sso-configuration-import-flow.png │ ├── org-events-flow.png │ ├── permission-set-lifecycle.png │ ├── sso-events-flow.png │ └── success-rate.jpg ├── samples │ ├── links_data │ │ ├── account%121111112211%Billing-ps%team-Accountants%GROUP%ssofile │ │ ├── account_tag%team^DataScientists%DataScientist-ps%team-DataScientists%GROUP%ssofile │ │ ├── ou_id%ou_id123435%SecurityAuditor-ps%team-SecurityAuditors%GROUP%ssofile │ │ ├── root%all%CloudOperator-ps%team-CloudOperators%GROUP%ssofile │ │ ├── root%all%CloudOperator-ps%user-breakglass%USER%ssofile │ │ └── root%all%SysAdmin-ps%team-SysAdmins%GROUP%ssofile │ ├── permission_sets │ │ ├── Billing-ps.json │ │ ├── CloudOperator-ps.json │ │ ├── DataScientist-ps.json │ │ ├── SecurityAuditor-ps.json │ │ └── SysAdmin-ps.json │ └── postman-collection │ │ └── aws-sso-extensions-for-enterprise.postman_collection.json └── wsd-code │ ├── account-assignment-lifecycle.wsd │ ├── current-aws-sso-configuration-import-flow.wsd │ ├── org-events-flow.wsd │ ├── permission-set-lifecycle.wsd │ └── sso-events-flow.wsd ├── jest.config.js ├── lib ├── build │ ├── buildConfig.ts │ └── regionSwitchBuildConfig.ts ├── constructs │ ├── access-manager.ts │ ├── cross-account-role.ts │ ├── fetch-cross-stack-values.ts │ ├── helpers.ts │ ├── import-artefacts.ts │ ├── independent-utlity.ts │ ├── lambda-layers.ts │ ├── lambda-proxy-api.ts │ ├── link-crud.ts │ ├── link-processor.ts │ ├── observability-artefacts.ts │ ├── org-events.ts │ ├── permission-set-crud.ts │ ├── permission-set-processor.ts │ ├── preSolution-access-manager.ts │ ├── ssm-param-reader.ts │ ├── ssm-param-writer.ts │ ├── sso-group-processor.ts │ └── utility.ts ├── lambda-functions │ ├── application-handlers │ │ └── src │ │ │ ├── groupsCud.ts │ │ │ ├── linkManager.ts │ │ │ ├── linkTopicProcessor.ts │ │ │ ├── managedPolicyQueueProcessor.ts │ │ │ ├── orgEvents.ts │ │ │ ├── permissionSetSync.ts │ │ │ ├── permissionSetTopicProcessor.ts │ │ │ ├── processTargetAccountSMListener.ts │ │ │ └── usersCud.ts │ ├── current-config-handlers │ │ └── src │ │ │ ├── import-account-assignments.ts │ │ │ ├── import-customermanagedpolicies-permissionsboundary.ts │ │ │ ├── import-permission-sets.ts │ │ │ ├── trigger-parentSM.ts │ │ │ └── update-custom-resource.ts │ ├── custom-waiters │ │ └── src │ │ │ ├── waitUntilAccountAssignmentCreation.ts │ │ │ ├── waitUntilAccountAssignmentDeletion.ts │ │ │ └── waitUntilPermissionSetProvisioned.ts │ ├── helpers │ │ └── src │ │ │ ├── interfaces.ts │ │ │ ├── isoDurationUtility.ts │ │ │ ├── payload-validator.ts │ │ │ └── utilities.ts │ ├── managed-policy-handlers │ │ └── src │ │ │ ├── describeOpIterator.ts │ │ │ ├── processCustomerManagedPolicy.ts │ │ │ └── processManagedPolicy.ts │ ├── package.json │ ├── region-switch │ │ └── src │ │ │ ├── rs-create-permission-sets.ts │ │ │ ├── rs-import-account-assignments.ts │ │ │ ├── rs-import-permission-sets.ts │ │ │ ├── trigger-deploySM.ts │ │ │ ├── trigger-parentSM.ts │ │ │ └── update-custom-resource.ts │ ├── upgrade-to-v303 │ │ └── src │ │ │ ├── processLinkData.ts │ │ │ ├── triggerV303SM.ts │ │ │ └── update-custom-resource.ts │ ├── user-interface-handlers │ │ └── src │ │ │ ├── linkApi.ts │ │ │ ├── linkCu.ts │ │ │ ├── linkDel.ts │ │ │ ├── permissionSetApi.ts │ │ │ ├── permissionSetCu.ts │ │ │ └── permissionSetDel.ts │ └── yarn.lock ├── lambda-layers │ └── nodejs-layer │ │ └── nodejs │ │ ├── package.json │ │ └── yarn.lock ├── payload-schema-definitions │ ├── Link-API.json │ ├── Link-S3.json │ ├── PermissionSet-DeleteAPI.json │ ├── PermissionSet-createUpdateAPI.json │ └── PermissionSet-createUpdateS3.json ├── stacks │ ├── pipeline │ │ ├── aws-sso-extensions-for-enterprise.ts │ │ └── pipeline-stages.ts │ ├── pipelineStageStacks │ │ ├── managed-policies.ts │ │ ├── org-events-processor.ts │ │ ├── pre-solution-artefacts.ts │ │ ├── solution-artefacts.ts │ │ ├── sso-api-roles.ts │ │ ├── sso-events-processor.ts │ │ ├── sso-import-artefacts-part1.ts │ │ ├── sso-import-artefacts-part2.ts │ │ └── upgrade-to-v303.ts │ └── region-switch │ │ ├── aws-sso-extensions-region-switch-deploy.ts │ │ └── aws-sso-extensions-region-switch-discover.ts └── state-machines │ ├── customer-managed-policies-asl.json │ ├── deploy-region-switch-objects-asl.json │ ├── import-account-assignments-asl.json │ ├── import-current-config-asl.json │ ├── import-permission-sets-asl.json │ ├── managed-policies-asl.json │ ├── processTargetAccounts-asl.json │ └── upgrade-to-v303-asl.json ├── package.json ├── test └── aws-sso-extensions-for-enterprise.ts ├── tsconfig.json └── yarn.lock /.adr-dir: -------------------------------------------------------------------------------- 1 | docs/adr 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | ignorePatterns: ["**/*.js", "cdk.out"], 6 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | parser: "@typescript-eslint/parser", 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | sourceType: "module", 11 | }, 12 | plugins: ["@typescript-eslint", "security"], 13 | rules: { 14 | "linebreak-style": ["error", "unix"], 15 | "@typescript-eslint/no-non-null-assertion": "off", 16 | eqeqeq: ["error", "always", { null: "ignore" }], 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": [ 19 | "warn", 20 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore transpiled files 2 | *.js 3 | *.d.ts 4 | 5 | # Node modules 6 | node_modules 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | 12 | # Parcel default cache directory 13 | .parcel-cache 14 | 15 | # DrawIO Image definitions 16 | *.drawio 17 | 18 | # Jest config 19 | !jest.config.js 20 | 21 | # Eslint config 22 | !.eslintrc.js 23 | 24 | # Prettier config for JSDOCS 25 | !.prettierrc 26 | 27 | # VS code specific ignores 28 | .vscode 29 | ~$*.docx 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 9, 3 | "node": true 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore transpiled files 2 | *.js 3 | *.d.ts 4 | 5 | # Node modules 6 | node_modules 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | 12 | # Parcel default cache directory 13 | .parcel-cache 14 | 15 | # DrawIO Image definitions 16 | *.drawio 17 | 18 | # Jest config 19 | !jest.config.js 20 | 21 | # Eslint config 22 | !.eslintrc.js 23 | 24 | # Lambda code exclusion 25 | # No type definitions available for dynamo-stream-diff library used by permission set stream handler 26 | !/lib/lambda-functions/ddb-stream-handlers/src/permissionSet.js 27 | 28 | # Ignore Python lambda layers except requirements.txt 29 | /lib/lambda-layers/python-layer/* 30 | !/lib/lambda-layers/python-layer/requirements.txt 31 | 32 | # VS code specific ignores 33 | .vscode 34 | ~$*.docx 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-jsdoc"] 3 | } 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | - A reproducible test case or series of steps 17 | - The version of our code being used 18 | - Any modifications you've made relevant to the bug 19 | - Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the _main_ branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | ## Finding contributions to work on 42 | 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 44 | 45 | ## Code of Conduct 46 | 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | ## Security issue notifications 52 | 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Amazon Web Services, Inc. or its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/aws-sso-extensions-for-enterprise.ts", 3 | "context": {} 4 | } 5 | -------------------------------------------------------------------------------- /cfn-nag/cfn_nag_ignored_rules.yaml: -------------------------------------------------------------------------------- 1 | RulesToSuppress: 2 | - id: W12 3 | reason: Default policies from Lambda (ServiceRoleDefaultPolicy) and pipeline (SelfMutationRoleDefaultPolicy and AssetsFileRoleDefaultPolicy) include * in resources 4 | - id: W28 5 | reason: Resources with explicit names are explicit 6 | - id: W35 7 | reason: Buckets without access logging are either access logs bucket (or) pipeline artefacts bucket which is managed by the L3 CDK pipeline construct and not accessible at synth stage 8 | - id: W51 9 | reason: False positives, S3 buckets have read/write access configured through the solution. 10 | - id: W58 11 | reason: False positives, ServiceDefaultRolePolicy includes permission. 12 | - id: W64 13 | reason: API gateway default usage plans are not modified, low throughput 14 | - id: W68 15 | reason: API gateway default usage plans are not modified, low throughput 16 | - id: W76 17 | reason: IAM policy definitions are derived through L2 CDK constructs 18 | - id: W84 19 | reason: Not encrypting cloudwatch log groups configured for API gateway access logs 20 | - id: W89 21 | reason: Not accesing any private VPC resources 22 | - id: W92 23 | reason: Concurrency not reserved, low throughput 24 | - id: F19 25 | reason: Only instance where the KMS key is not rotated is generated by L3 CDK pipeline construct and not accessible at synth stage 26 | -------------------------------------------------------------------------------- /config/env.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | App: "aws-sso-extensions-for-enterprise" 3 | Environment: "env" 4 | Version: "3.1.9" 5 | 6 | PipelineSettings: 7 | BootstrapQualifier: "" # For example: 'ssoutility' 8 | DeploymentAccountId: "" 9 | DeploymentAccountRegion: "" 10 | TargetAccountId: "" 11 | TargetAccountRegion: "" 12 | OrgMainAccountId: "" 13 | SSOServiceAccountId: "" 14 | SSOServiceAccountRegion: "" 15 | RepoType: "CODECOMMIT" # Allowed values - ["S3", "CODECOMMIT", "CODESTAR"] 16 | RepoArn: "arn:aws:codecommit:::aws-sso-extensions-for-enterprise" # Only required if RepoType is "CODECOMMIT" 17 | RepoName: "aws-samples/aws-iam-identity-center-extensions" # Only required if RepoType is "CODESTAR". Ensure this is the fully qualified repository name like "aws-samples/aws-iam-identity-center-extensions". 18 | CodeStarConnectionArn: "arn:aws:codeconnections:us-east-1:686255979076:connection/12c162f9-8c00-4bcb-9aeb-6d42b072760b" # Only required if RepoType is "CODESTAR" 19 | RepoBranchName: "main" # Verify that this is the branch name used by your repository if RepoType is "CODESTAR" or "CODECOMMIT" 20 | SourceBucketName: "" # Ensure this bucket exists in the deployment account. Required if RepoType is "S3" 21 | SourceObjectKey: "" # Ensure the source code is uploaded to this location in the bucket. Required if RepoType is "S3". 22 | SynthCommand: "yarn cdk-synth-env" 23 | 24 | Parameters: 25 | LinksProvisioningMode: "api" # Allowed values - ["api", "s3"] 26 | PermissionSetProvisioningMode: "api" # Allowed values - ["api", "s3"] 27 | LinkCallerRoleArn: "arn:aws:iam:::role/LinkCallerRole" 28 | PermissionSetCallerRoleArn: "arn:aws:iam:::role/PermissionSetCallerRole" 29 | NotificationEmail: "" 30 | AccountAssignmentVisibilityTimeoutHours: 2 # Adjust this number based on the maximum no of concurrent account assignments you're targeting for. Refer to "Scaling for large organizations" section in README.md for details 31 | IsAdUsed: false 32 | DomainName: "corp.example.com" # If IsAdUsed is false, this will be ignored. 33 | ImportCurrentSSOConfiguration: false # Set this to true if you want the solution to do a one-time import of your current AWS IAM Identity Center permission sets and account assignments. Refer to "Import existing AWS IAM Identity Center access entitlements for management through the solution" section under "Features" in README.md for details 34 | UpgradeFromVersionLessThanV303: false # Should be set to true, if upgrading from solution version 3.0.2/earlier 35 | SupportNestedOU: false # Set this to true if your preference is for the solution to support nested OU's as part of account assignments. Refer to "Enterprise friendly account assignment life cycle" and "Automated access change management for root, ou_id and account_tag scopes" sections under "Features" in README.md for details 36 | FunctionLogMode: "Info" # Used for configuring lambda function logging level. Alowed values - ["Debug","Info","Warn","Exception"] 37 | -------------------------------------------------------------------------------- /config/region-switch.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | BootstrapQualifier: "ssoutility" 3 | SSOServiceAccountId: "" 4 | SSOServiceAccountRegion: "" 5 | SSOServiceTargetAccountRegion: "" 6 | -------------------------------------------------------------------------------- /docs/adr/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2022-08-22 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | As far as possible, all decisions must be [two-way door decision](https://aws.amazon.com/executive-insights/content/how-amazon-defines-and-operationalizes-a-day-1-culture/#Make_high_quality.2C_high_velocity_decisions). 18 | 19 | ## Consequences 20 | 21 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 22 | -------------------------------------------------------------------------------- /docs/architecture/decisions/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2022-09-05 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /docs/architecture/decisions/0002-continuous-sync-for-permission-sets.md: -------------------------------------------------------------------------------- 1 | # 2. Continuous Sync For Permission Sets 2 | 3 | Date: 2022-09-07 4 | 5 | ## Status 6 | 7 | Proposed 8 | 9 | ## Context 10 | 11 | When permission sets are created/updated/deleted through interfaces outside the solution i.e. console , SDK etc, the solution is not made aware of this change. This would result in a stale data scenario where the solution's version of the permission set is different to what it was in the AWS IAM Identity Center instance, often resulting in a deadlock situation where the solution user cannot manage/ cannot correctly manage this permission set through the solution interfaces. 12 | 13 | ## Decision 14 | 15 | To handle this, we intend to have continuous sync for permission sets where in , through event bridge rules we listen on the following types of permission set changes, and update solution's repository to be aware of these changes and ensure that the permission set CRUD flow triggered through the solution interfaces is in line with these changes. We will import all changes from all permission sets as part of these continous sync operations 16 | 17 | 1. Permission set create/update/delete operation 18 | 2. Permission set managed policy changes - AWS and customer 19 | 3. Permission set inline policy changes 20 | 4. Permission set permissions boundary changes 21 | 5. Permission set tags changes 22 | 23 | ## Consequences 24 | 25 | This change will bring permission sets provisioned/managed outside the solution into the scope of the solution. It would allow solution users to provision account assignments to these imported permission sets and manage these imported permission sets through solution interfaces as well. 26 | -------------------------------------------------------------------------------- /docs/architecture/decisions/0003-continuous-sync-for-account-assignments.md: -------------------------------------------------------------------------------- 1 | # 3. Continuous sync for Account Assignments 2 | 3 | Date: 2022-09-07 4 | 5 | ## Status 6 | 7 | Proposed 8 | 9 | ## Context 10 | 11 | When account assignments are created/deleted through interfaces outside the solution i.e. console , SDK etc, the solution is not made aware of this change. This would result in a stale data scenario where the solution's version of the account assignment is different to waht it was in the AWS IAM Identity Center instance, resulting in either an invalid account assignment create/delet operation 12 | 13 | ## Decision 14 | 15 | To handle this, we intend to have continous sync for account assignments where in, through event bridge rules we listen on the following types of account assignment changes, and update solution's repository to be aware of these changes and ensure that the account assignment create/delete flow triggered through the solution intefaces is in line with these changes. We will import all create/delete account assignments as part of these continuous sync operations 16 | 17 | ## Consequences 18 | 19 | This change will bring account assignments created/deleted outside the solution into the scope of the solution. 20 | -------------------------------------------------------------------------------- /docs/architecture/decisions/0004-region-switch-enhancements.md: -------------------------------------------------------------------------------- 1 | # 4. Region Switch Enhancements 2 | 3 | Date: 2022-09-07 4 | 5 | ## Status 6 | 7 | Proposed 8 | 9 | ## Context 10 | 11 | - Region Switch functionality in the solution currently only caters for an on-demand region switch capability. Solution user will need to run a `discover` phase in their current AWS region first, followed by a `deploy` phase in their new AWS region that would allow them to automatically import permission sets and account assignments in the new AWS region 12 | - There's however interest for customers to use the solution's region switch `discover` capability on a continous basis. That is, customers would prefer to deploy the region switch `discover` capability, so that it's continuously in sync with changes in permission sets/account assignments happening on AWS IAM Identity Center instance configuration, allowing them to do a `deploy` on a new AWS region whenever they choose. 13 | 14 | ## Decision 15 | 16 | - To support this, region switch functionality would be extended with the continuous sync features, so that customers can choose `continous-sync` as the region switch flavour 17 | - This will then enable customers to have an up-to-date copy of permisison sets/account assignments that they could choose to `deploy` into a new AWS region at a time of their choosing. 18 | 19 | ## Consequences 20 | 21 | Continous sync feature needs to be de-coupled from the solution to enable it to be deployed both with the core solution as well as region switch components 22 | -------------------------------------------------------------------------------- /docs/documentation/Region-Switch.md: -------------------------------------------------------------------------------- 1 | # AWS IAM Identity Center Region Switch 2 | 3 | AWS IAM Identity Center service is single-region at this point of time. In some instances, customers wish to move their AWS IAM Identity Center configuration from one region to another region and this document explains how some of these migration activities could be automated. 4 | 5 | ## Caveats 6 | 7 | - AWS IAM Identity Center at this point of time does not have API's to manage identities , instance enablement. These are manual operations. 8 | - When a customer migrates AWS IAM Identity Center from one region to another region, the solution only helps automate migration of permission sets and account assignments. 9 | - The solution assumes that identities (users/groups) are onboarded into the new region using the same naming convention. For ex, if a customer had onboarded a user with user name `alpha-user`, group with display name `beta-group` in region 1 through any of the supported identity sources, the solution assumes that the customer will onboard the user with the same user name `alpha-user` and same group display name `beta-group` in region 2. Only when this condition is met, the solution automatically migrates account assignments from region 1 to region 2. 10 | 11 | ## Sequence 12 | 13 | - `Discover` component of the solution is deployed in your current AWS IAM Identity Center account and current AWS IAM Identity Center region first. This would read all the permission sets, account assignments in your current AWS IAM Identity Center region and persist them for later usage 14 | - The customer then manually moves the AWS IAM Identity Center configuration from their current region to the new region 15 | - The customer onboards all the required identities in the new region 16 | - `Deploy` component of the solution is deployed in your current AWS IAM Identity Center Account and new AWS IAM Identity Center region. This would then deploy all the permission sets and account assignments similar to how they were provisioned in the old AWS IAM Identity Center region. 17 | - `Destroy` components of the solution are then run to remove the artefacts created in the `Discover` and `Deploy` phase. 18 | 19 | ## Execute 20 | 21 | - Ensure the following [pre-requisites](https://catalog.us-east-1.prod.workshops.aws/workshops/640b0bab-1f5e-494a-973e-4ed7919d397b/en-US/00-prerequisites) are ready and available 22 | - Clone the solution code 23 | 24 | ```bash 25 | git clone https://github.com/aws-samples/aws-sso-extensions-for-enterprise.git solution-code 26 | ``` 27 | 28 | - From the root of the project run `yarn install --frozen-lock-file` 29 | - Navigate to `lib\lambda-layers\nodejs-layer\nodejs` and run `yarn install --frozen-lock-file` 30 | - Set the environment variables in your shell 31 | 32 | ```bash 33 | export BOOTSTRAP_QUALIFIER="ssoutility" 34 | export CFN_EXECUTION_POLICIES="arn:aws:iam::aws:policy/AdministratorAccess" 35 | export CONFIG="region-switch-discover" 36 | export SSO_PROFILE= 37 | export SSO_ACCOUNT= 38 | export SSO_REGION= 39 | ``` 40 | 41 | - Using your org main (i.e. SSO service account) and current AWS IAM Identity Center region credentials, run the following steps 42 | 43 | ```bash 44 | yarn cdk bootstrap --qualifier $BOOTSTRAP_QUALIFIER \ 45 | --cloudformation-execution-policies $CFN_EXECUTION_POLICIES \ 46 | aws://$SSO_ACCOUNT/$SSO_REGION \ 47 | -c config=$CONFIG \ 48 | --profile $SSO_PROFILE \ 49 | --region $SSO_REGION 50 | ``` 51 | 52 | - Update your environment variables to match the new AWS IAM Identity Center region 53 | 54 | ```bash 55 | export CONFIG="region-switch-deploy" 56 | export SSO_REGION= 57 | ``` 58 | 59 | - Using your org main (i.e. SSO service account) and new AWS IAM Identity Center region credentials, run the following steps 60 | 61 | ```bash 62 | yarn cdk bootstrap --qualifier $BOOTSTRAP_QUALIFIER \ 63 | --cloudformation-execution-policies $CFN_EXECUTION_POLICIES \ 64 | aws://$SSO_ACCOUNT/$SSO_REGION \ 65 | -c config=$CONFIG \ 66 | --profile $SSO_PROFILE \ 67 | --region $SSO_REGION 68 | ``` 69 | 70 | - Update `config\region-switch.yaml` file with your environment values 71 | 72 | ```yaml 73 | BootstrapQualifier: "ssoutility" 74 | SSOServiceAccountId: "" 75 | SSOServiceAccountRegion: "" 77 | ``` 78 | 79 | - Run `Discover` phase through the following steps by using your Orgmain account and current AWS IAM Identity Center region credentials: 80 | - Validate that the configuration and other dependencies are all set up by running `yarn synth-region-switch-discover` from the root of the project. 81 | - This should not return any errors and should synthesise successfully 82 | - Run `deploy-region-switch-discover` from the root of the project. Wait until the discover phase Cloudformation stacks are successfully deployed. 83 | - Set up AWS IAM Identity Center in the new region, set up identity store and onboard all the identities in the new AWS IAM Identity Center region, refer to service documentation [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/getting-started.html). 84 | - Identiies must be on-boarded into the new AWS IAM Identity Center region before running the next step. 85 | - Run `Deploy` phase through the following steps by using your Orgmain account and new AWS IAM Identity Center region credentials: 86 | - Validate that the configuration and other dependencies are all set up by running `yarn synth-region-switch-deploy` from the root of the project. 87 | - This should not return any errors and should synthesise successfully 88 | - Run `deploy-region-switch` from the root of the project. Wait until the deploy phase Cloudformation stacks are successfully deployed. 89 | - Verify that all your account assignments and permission sets are successfully created in the new AWS IAM Identity Center region 90 | - Post verification that everything is deployed correctly in the new AWS IAM Identity Center region, delete the artefacts created for `Deploy` and `Discover` phases by running the following: 91 | - Using Orgmain and new AWS IAM Identity Center region credentials, run `yarn destroy-region-switch-deploy` from the root of the project. This will remove all the deploy phase artefacts. 92 | - Using Orgmain and old AWS IAM Identity Center region credentials, run `yarn destroy-region-switch-discover` from the root of the project. This will remove all the discover phase artefacts 93 | -------------------------------------------------------------------------------- /docs/documentation/Scaling.md: -------------------------------------------------------------------------------- 1 | # Scaling the solution for large enterprises 2 | 3 | - The solution is designed to scale for any organization size while ensuring that it works without being throttled against the [default AWS IAM Identity Center Admin API quota](https://docs.aws.amazon.com/singlesignon/latest/userguide/limits.html) 4 | 5 | - The solution has been tested with the following load parameters: 6 | 7 | - For an AWS organizational unit with **60 accounts, 5 account assignments at the OU scope were created and deleted concurrently (within one minute)**. 8 | - This resulted in **1200 account assignment operations (600 create, 600 delete)** being posted to the AWS IAM Identity Center instance. 9 | - The solution processed all the **1200 account assignment operations in 61 minutes with 100% success rate**. 10 | 11 | ## Account assignment processing metrics 12 | 13 | [![Duration](../images/average-duration.jpg)](../images/average-duration.jpg) 14 | 15 | [![Concurrent Execution](../images/concurrent-executions.jpg)](../images/concurrent-executions.jpg) 16 | 17 | [![Success rate and Error count](../images/success-rate.jpg)](../images/success-rate.jpg) 18 | 19 | ## Tuning the solution for larger environments 20 | 21 | - The solution has a configurable visibility timeout parameter for the messages in the account assignment queue. This is defined in **hours** as part of your environment configuration file. 22 | - The solution sets this to a default value of 2 hours. 23 | - This timeout has been tested to work up to 1200 **concurrent** account assignment operations with the worst case scenario of a 100% redrive in the message queue. 24 | - If your target concurrent account assignment operations is higher than 1200, the timeout value should be linearly scaled. For ex, if you are targeting 3600 **concurrent** assignment operations , then the timeout value should be set to 6 hours to cater for the worst case scenario of 100% redrive in the message queue. 25 | -------------------------------------------------------------------------------- /docs/documentation/Use-Case-Flows.md: -------------------------------------------------------------------------------- 1 | # Use Case Logical State Diagrams 2 | 3 | For more information on going through the use cases for this solution, see the workshop [here](https://catalog.workshops.aws/ssoextensions/en-US/03-usecases) 4 | 5 | ## Account Assignment Life Cycle 6 | 7 | [![Account Assignment life cycle](../images/account-assignment-lifecycle.png)](../images/account-assignment-lifecycle.png) 8 | 9 | ## Permission Set Life Cycle 10 | 11 | [![Permission Set life cycle](../images/permission-set-lifecycle.png)](../images/permission-set-lifecycle.png) 12 | 13 | ## Org Events Flow 14 | 15 | [![Org events flow](../images/org-events-flow.png)](../images/org-events-flow.png) 16 | 17 | ## SSO Events Flow 18 | 19 | [![SSO events flow](../images/sso-events-flow.png)](../images/sso-events-flow.png) 20 | 21 | ## Current AWS IAM Identity Center Configuration Import flow 22 | 23 | [![Current AWS IAM Identity Center Configuration Import flow](../images/current-aws-sso-configuration-import-flow.png)](../images/current-aws-sso-configuration-import-flow.png) 24 | -------------------------------------------------------------------------------- /docs/images/account-assignment-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/account-assignment-lifecycle.png -------------------------------------------------------------------------------- /docs/images/average-duration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/average-duration.jpg -------------------------------------------------------------------------------- /docs/images/aws-sso-extensions-for-enterprise-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/aws-sso-extensions-for-enterprise-overview.png -------------------------------------------------------------------------------- /docs/images/concurrent-executions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/concurrent-executions.jpg -------------------------------------------------------------------------------- /docs/images/current-aws-sso-configuration-import-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/current-aws-sso-configuration-import-flow.png -------------------------------------------------------------------------------- /docs/images/org-events-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/org-events-flow.png -------------------------------------------------------------------------------- /docs/images/permission-set-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/permission-set-lifecycle.png -------------------------------------------------------------------------------- /docs/images/sso-events-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/sso-events-flow.png -------------------------------------------------------------------------------- /docs/images/success-rate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/images/success-rate.jpg -------------------------------------------------------------------------------- /docs/samples/links_data/account%121111112211%Billing-ps%team-Accountants%GROUP%ssofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/samples/links_data/account%121111112211%Billing-ps%team-Accountants%GROUP%ssofile -------------------------------------------------------------------------------- /docs/samples/links_data/account_tag%team^DataScientists%DataScientist-ps%team-DataScientists%GROUP%ssofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/samples/links_data/account_tag%team^DataScientists%DataScientist-ps%team-DataScientists%GROUP%ssofile -------------------------------------------------------------------------------- /docs/samples/links_data/ou_id%ou_id123435%SecurityAuditor-ps%team-SecurityAuditors%GROUP%ssofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/samples/links_data/ou_id%ou_id123435%SecurityAuditor-ps%team-SecurityAuditors%GROUP%ssofile -------------------------------------------------------------------------------- /docs/samples/links_data/root%all%CloudOperator-ps%team-CloudOperators%GROUP%ssofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/samples/links_data/root%all%CloudOperator-ps%team-CloudOperators%GROUP%ssofile -------------------------------------------------------------------------------- /docs/samples/links_data/root%all%CloudOperator-ps%user-breakglass%USER%ssofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/samples/links_data/root%all%CloudOperator-ps%user-breakglass%USER%ssofile -------------------------------------------------------------------------------- /docs/samples/links_data/root%all%SysAdmin-ps%team-SysAdmins%GROUP%ssofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iam-identity-center-extensions/b213f8f5b667ae72a8a9887b683ba5bb8838d8e5/docs/samples/links_data/root%all%SysAdmin-ps%team-SysAdmins%GROUP%ssofile -------------------------------------------------------------------------------- /docs/samples/permission_sets/Billing-ps.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissionSetName": "Billing-ps", 3 | "sessionDurationInMinutes": "60", 4 | "relayState": "https://eu-west-1.console.aws.amazon.com/console/home?region=eu-west-1#", 5 | "tags": [ 6 | { 7 | "Key": "versionid", 8 | "Value": "01" 9 | }, 10 | { 11 | "Key": "team", 12 | "Value": "Accountants" 13 | } 14 | ], 15 | "managedPoliciesArnList": ["arn:aws:iam::aws:policy/job-function/Billing"], 16 | "inlinePolicyDocument": {} 17 | } 18 | -------------------------------------------------------------------------------- /docs/samples/permission_sets/CloudOperator-ps.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissionSetName": "CloudOperator-ps", 3 | "sessionDurationInMinutes": "240", 4 | "relayState": "https://eu-west-1.console.aws.amazon.com/console/home?region=eu-west-1#", 5 | "tags": [ 6 | { 7 | "Key": "versionid", 8 | "Value": "01" 9 | }, 10 | { 11 | "Key": "team", 12 | "Value": "CloudOperators" 13 | } 14 | ], 15 | "managedPoliciesArnList": [ 16 | "arn:aws:iam::aws:policy/job-function/SystemAdministrator", 17 | "arn:aws:iam::aws:policy/job-function/NetworkAdministrator" 18 | ], 19 | "inlinePolicyDocument": { 20 | "Version": "2012-10-17", 21 | "Statement": [ 22 | { 23 | "Action": [ 24 | "iam:AddRoleToInstanceProfile", 25 | "iam:CreateInstanceProfile", 26 | "iam:CreatePolicy", 27 | "iam:CreatePolicyVersion", 28 | "iam:DeleteInstanceProfile", 29 | "iam:DeletePolicy", 30 | "iam:DeleteRole", 31 | "iam:PassRole", 32 | "iam:UpdateRole", 33 | "iam:DeleteRolePermissionsBoundary", 34 | "iam:UpdateRoleDescription", 35 | "iam:RemoveRoleFromInstanceProfile" 36 | ], 37 | "Resource": [ 38 | "arn:aws:iam::*:role/Application_*", 39 | "arn:aws:iam::*:policy/Application_*", 40 | "arn:aws:iam::*:instance-profile/Application_*" 41 | ], 42 | "Effect": "Allow", 43 | "Sid": "AllowOtherIAMActions" 44 | }, 45 | { 46 | "Action": "iam:List*", 47 | "Resource": "*", 48 | "Effect": "Allow", 49 | "Sid": "AllowReadIAMActions" 50 | } 51 | ] 52 | }, 53 | "customerManagedPoliciesList": [ 54 | { 55 | "Name": "cmp-1", 56 | "Path": "/cmp/1/" 57 | }, 58 | { 59 | "Name": "cmp-2", 60 | "Path": "/cmp/2/" 61 | }, 62 | { 63 | "Name": "cmp-3" 64 | } 65 | ], 66 | "permissionsBoundary": { 67 | "ManagedPolicyArn": "arn:aws:iam::aws:policy/job-function/NetworkAdministrator" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/samples/permission_sets/DataScientist-ps.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissionSetName": "DataScientist-ps", 3 | "sessionDurationInMinutes": "300", 4 | "relayState": "https://eu-west-1.console.aws.amazon.com/console/home?region=eu-west-1#", 5 | "tags": [ 6 | { 7 | "Key": "versionid", 8 | "Value": "01" 9 | }, 10 | { 11 | "Key": "team", 12 | "Value": "DataScientists" 13 | } 14 | ], 15 | "managedPoliciesArnList": [ 16 | "arn:aws:iam::aws:policy/job-function/DataScientist" 17 | ], 18 | "inlinePolicyDocument": {} 19 | } 20 | -------------------------------------------------------------------------------- /docs/samples/permission_sets/SecurityAuditor-ps.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissionSetName": "SecurityAuditor-ps", 3 | "sessionDurationInMinutes": "60", 4 | "relayState": "https://eu-west-1.console.aws.amazon.com/console/home?region=eu-west-1#", 5 | "tags": [ 6 | { 7 | "Key": "versionid", 8 | "Value": "01" 9 | }, 10 | { 11 | "Key": "team", 12 | "Value": "SecurityAuditors" 13 | } 14 | ], 15 | "managedPoliciesArnList": ["arn:aws:iam::aws:policy/SecurityAudit"], 16 | "inlinePolicyDocument": {}, 17 | "permissionsBoundary": { 18 | "CustomerManagedPolicyReference": { 19 | "Name": "cmp-pb", 20 | "Path": "/cmp/pb/" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/samples/permission_sets/SysAdmin-ps.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissionSetName": "SysAdmin-ps", 3 | "sessionDurationInMinutes": "240", 4 | "relayState": "https://eu-west-1.console.aws.amazon.com/systems-manager/managed-instances?region=eu-west-1#", 5 | "tags": [ 6 | { 7 | "Key": "versionid", 8 | "Value": "01" 9 | }, 10 | { 11 | "Key": "team", 12 | "Value": "SysAdmins" 13 | } 14 | ], 15 | "managedPoliciesArnList": [], 16 | "inlinePolicyDocument": { 17 | "Version": "2012-10-17", 18 | "Statement": [ 19 | { 20 | "Sid": "SSO", 21 | "Effect": "Allow", 22 | "Action": [ 23 | "sso:ListDirectoryAssociations*", 24 | "identitystore:DescribeUser" 25 | ], 26 | "Resource": "*" 27 | }, 28 | { 29 | "Sid": "EC2", 30 | "Effect": "Allow", 31 | "Action": ["ec2:DescribeInstances", "ec2:GetPasswordData"], 32 | "Resource": "*" 33 | }, 34 | { 35 | "Sid": "SSM", 36 | "Effect": "Allow", 37 | "Action": [ 38 | "ssm:DescribeInstanceProperties", 39 | "ssm:GetCommandInvocation", 40 | "ssm:GetInventorySchema" 41 | ], 42 | "Resource": "*" 43 | }, 44 | { 45 | "Sid": "TerminateSession", 46 | "Effect": "Allow", 47 | "Action": ["ssm:TerminateSession"], 48 | "Resource": "*", 49 | "Condition": { 50 | "StringLike": { 51 | "ssm:resourceTag/aws:ssmmessages:session-id": ["${aws:userName}"] 52 | } 53 | } 54 | }, 55 | { 56 | "Sid": "SSMGetDocument", 57 | "Effect": "Allow", 58 | "Action": ["ssm:GetDocument"], 59 | "Resource": [ 60 | "arn:aws:ssm:*:*:document/AWS-StartPortForwardingSession", 61 | "arn:aws:ssm:*:*:document/SSM-SessionManagerRunShell" 62 | ] 63 | }, 64 | { 65 | "Sid": "SSMStartSession", 66 | "Effect": "Allow", 67 | "Action": ["ssm:StartSession"], 68 | "Resource": [ 69 | "arn:aws:ec2:*:*:instance/*", 70 | "arn:aws:ssm:*:*:managed-instance/*", 71 | "arn:aws:ssm:*:*:document/AWS-StartPortForwardingSession" 72 | ], 73 | "Condition": { 74 | "BoolIfExists": { 75 | "ssm:SessionDocumentAccessCheck": "true" 76 | } 77 | } 78 | }, 79 | { 80 | "Sid": "SSMSendCommand", 81 | "Effect": "Allow", 82 | "Action": ["ssm:SendCommand"], 83 | "Resource": [ 84 | "arn:aws:ec2:*:*:instance/*", 85 | "arn:aws:ssm:*:*:managed-instance/*", 86 | "arn:aws:ssm:*:*:document/AWSSSO-CreateSSOUser" 87 | ], 88 | "Condition": { 89 | "BoolIfExists": { 90 | "ssm:SessionDocumentAccessCheck": "true" 91 | } 92 | } 93 | }, 94 | { 95 | "Sid": "GuiConnect", 96 | "Effect": "Allow", 97 | "Action": [ 98 | "ssm-guiconnect:CancelConnection", 99 | "ssm-guiconnect:GetConnection", 100 | "ssm-guiconnect:StartConnection" 101 | ], 102 | "Resource": "*" 103 | } 104 | ] 105 | }, 106 | "customerManagedPoliciesList": [ 107 | { 108 | "Name": "cmp-1", 109 | "Path": "/cmp/1/" 110 | } 111 | ], 112 | "permissionsBoundary": { 113 | "CustomerManagedPolicyReference": { 114 | "Name": "cmp-pb", 115 | "Path": "/cmp/pb/" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /docs/wsd-code/account-assignment-lifecycle.wsd: -------------------------------------------------------------------------------- 1 | title Account Assignment Life cycle 2 | 3 | actor Solution User 4 | 5 | Solution User->User Interface Handler: Post account assignment operation 6 | note over User Interface Handler: API / S3 based interfaces 7 | User Interface Handler->User Interface Handler: validatePayloadSchema() 8 | alt valid payload: 9 | activate User Interface Handler 10 | User Interface Handler->Solution Persistence(DynamoDB): Create/Update/Delete payload 11 | User Interface Handler->Solution Persistence(S3): Create/Update/Delete payload 12 | User Interface Handler->Topic Processor: Post payload and action 13 | User Interface Handler->Solution User: return success with requestId for tracing 14 | activate Topic Processor 15 | Topic Processor->Solution Persistence(DynamoDB): doesPermissionSetExist() ? 16 | alt Permission set exists 17 | Topic Processor->AWS IAM Identity Center Identity Store: doesPrincipalExist()? 18 | AWS IAM Identity Center Identity Store->Topic Processor: return principal list result 19 | alt Principal exists 20 | Topic Processor->Topic Processor: checkScopeType? 21 | alt scopeType=account 22 | Topic Processor->Account assignment FIFO queue: Post account assignment creation/deletion payload 23 | Account assignment FIFO queue->Account assignment queue processor: Pull latest message for the message_group_id 24 | activate Account assignment queue processor 25 | Account assignment queue processor->AWS IAM Identity Center Admin API: Process account assignment operation 26 | Account assignment queue processor->AWS IAM Identity Center Admin API: waitUntilAccountAssignment create/delete 27 | AWS IAM Identity Center Admin API->Account assignment queue processor: respond AccountAssignment operation status 28 | Account assignment queue processor->Account assignment queue processor: determine if success/fail/time out and log the results 29 | Account assignment queue processor->Solution Persistence(DynamoDB): upsert/delete the provisioned/de-provisioned account assignment details 30 | deactivate Account assignment queue processor 31 | else scopeType=root/ou_id/account_tag 32 | Topic Processor->Org entities State Machine(Org account): Post account assignment operation details 33 | Org entities State Machine(Org account)->Org entitites processor: Resolve final list of target accounts 34 | Org entitites processor->Account assignment FIFO queue: Post account assignment creation/deletion payload 35 | note over Account assignment FIFO queue: same sequence as above for processing messages in the queue 36 | end 37 | else Principal does not exist 38 | Topic Processor->Topic Processor: log and complete the process 39 | end 40 | else Permission set does not exist 41 | Topic Processor->Topic Processor: log and complete the process 42 | end 43 | deactivate Topic Processor 44 | deactivate User Interface Handler 45 | else invalid payload: 46 | User Interface Handler->Solution User: Return validation error 47 | end 48 | -------------------------------------------------------------------------------- /docs/wsd-code/org-events-flow.wsd: -------------------------------------------------------------------------------- 1 | title Org Events Flow 2 | 3 | Org Events EventBridge Rule(Org account)->Org Events Processor: trigger matching org event 4 | Org Events Processor->Org Events Processor: determineEventType() 5 | alt Account creation event 6 | Org Events Processor->Solution Persistence(DynamoDB): lookup if there are any account assignments with scopeType = root 7 | alt resultSet.length >0 8 | Org Events Processor->Solution Persistence(DynamoDB): doesPermissionSetExist() ? 9 | alt Permission Set exists 10 | Org Events Processor->AWS IAM Identity Center Identity Store: doesPrincipalExist()? 11 | AWS IAM Identity Center Identity Store->Org Events Processor: return principal list result 12 | alt Principal exists 13 | Org Events Processor->Account assignment FIFO queue: post account assignment operation 14 | note over Account assignment FIFO queue: from this point, the same sequence\n as account assignment life cycle\n is followed 15 | else Principal does not exist 16 | Org Events Processor->Org Events Processor: log and complete the operation 17 | end 18 | else Permission set does not exist 19 | Org Events Processor->Org Events Processor: log and complete the operation 20 | end 21 | else resultSet.length = 0 22 | Org Events Processor->Org Events Processor: log and complete the operation 23 | end 24 | else Account move event 25 | Org Events Processor->Solution Persistence(DynamoDB): lookup if there are any account assignments with scopeType = ou_id and scopeValue = old ou_id 26 | alt resultSet.length >0 27 | note over Org Events Processor: from this point, the same sequence as \n Account creation event is followed 28 | else resultSet.length = 0 29 | Org Events Processor->Org Events Processor: log and complete the operation 30 | end 31 | Org Events Processor->Solution Persistence(DynamoDB): lookup if there are any account assignments with scopeType = ou_id and scopeValue = new ou_id 32 | alt resultSet.length >0 33 | note over Org Events Processor: from this point, the same sequence as \n Account creation event is followed 34 | else resultSet.length = 0 35 | Org Events Processor->Org Events Processor: log and complete the operation 36 | end 37 | else Tag Change on org resource event 38 | Org Events Processor->Org Events Processor: determine tagChange type? 39 | alt tag key-pair create/updated 40 | Org Events Processor->Solution Persistence(DynamoDB): lookup if there are any account assignments with scopeType = account_tag and scopeValue = tagKey^tagValue 41 | alt resultSet.length>0 42 | note over Org Events Processor: from this point, the same sequence as \n Account creation event is followed 43 | else resultSet.length = 0 44 | Org Events Processor->Solution Persistence(DynamoDB): lookup if there are any provisioned account assignments with tagKeyLookUp = tagKey^accountId 45 | alt resultSet.length >0 46 | Org Events Processor->Account assignment FIFO queue: Post delete account assignment operation 47 | else resultSet.length = 0 48 | Org Events Processor->Org Events Processor: log and complete the operation 49 | end 50 | end 51 | else tag key-pair deleted 52 | Org Events Processor->Solution Persistence(DynamoDB): lookup if there are any provisioned account assignments with tagKeyLookUp = tagKey^accountId 53 | alt resultSet.length >0 54 | Org Events Processor->Account assignment FIFO queue: Post delete account assignment operation 55 | else resultSet.length = 0 56 | Org Events Processor->Org Events Processor: log and complete the operation 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /docs/wsd-code/permission-set-lifecycle.wsd: -------------------------------------------------------------------------------- 1 | title Permission Set Life cycle 2 | 3 | actor Solution User 4 | 5 | Solution User->User Interface Handler: Post permission set operation 6 | note over User Interface Handler: API / S3 based interfaces 7 | User Interface Handler->User Interface Handler: validatePayloadSchema() 8 | alt valid payload: 9 | activate User Interface Handler 10 | User Interface Handler->Solution Persistence(DynamoDB): Create/Update/Delete payload 11 | User Interface Handler->Solution Persistence(S3): Create/Update/Delete payload 12 | User Interface Handler->Topic Processor: Post payload and action 13 | User Interface Handler->Solution User: return success with requestId for tracing 14 | deactivate User Interface Handler 15 | activate Topic Processor 16 | Topic Processor->Topic Processor: determineActionType() 17 | alt create: 18 | Topic Processor->Solution Persistence(DynamoDB): fetch permission set object 19 | Topic Processor->AWS IAM Identity Center Admin API: create permission set operation 20 | Topic Processor->Topic Processor: parse permissionSetArn from the result 21 | Topic Processor->Solution Persistence(DynamoDB): persist permissionSetArn 22 | Topic Processor->Topic Processor: determine if the permission set object has other attributes 23 | alt permission set has non-empty managed policies/inline policy document/tags 24 | Topic Processor->AWS IAM Identity Center Admin API: update permission set object 25 | end 26 | Topic Processor->Permission Set Sync processor: Post permission set sync notification 27 | Permission Set Sync processor->Solution Persistence(DynamoDB): check if there are related account assignments for this permission set 28 | Permission Set Sync processor->Permission Set Sync processor: check resultSet 29 | alt if resultSet.length > 0 30 | Permission Set Sync processor->Permission Set Sync processor: loop through the result set 31 | activate Permission Set Sync processor 32 | Permission Set Sync processor->AWS IAM Identity Center Identity Store: doesPrincipalExist() ? 33 | AWS IAM Identity Center Identity Store->Permission Set Sync processor: return principal list result 34 | alt Principal exists 35 | Permission Set Sync processor->Permission Set Sync processor: checkScopeType? 36 | alt scopeType=account 37 | Permission Set Sync processor->Account assignment FIFO queue: Post account assignment creation/deletion payload 38 | note over Account assignment FIFO queue: from this point, the solution follows\nsame sequence as account assignment\n life cycle 39 | else scopeType=root/ou_id/account_tag 40 | Permission Set Sync processor->Org entities State Machine(Org account): Post account assignment operation details 41 | note over Org entities State Machine(Org account): from this point, the solution\nfollows same sequence as\naccount assignment life cycle 42 | end 43 | else Principal does not exist 44 | Permission Set Sync processor->Permission Set Sync processor: log and complete the process 45 | end 46 | deactivate Permission Set Sync processor 47 | else resultSet.length = 0 48 | Permission Set Sync processor->Permission Set Sync processor: log and complete the process 49 | end 50 | else update: 51 | Topic Processor->Topic Processor: calculateDelta() 52 | alt deltaExists: 53 | Topic Processor->Topic Processor: Parse out the invididual delta elements 54 | Topic Processor->AWS IAM Identity Center Admin API: update permission set object with the individual delta elements 55 | Topic Processor->AWS IAM Identity Center Admin API: trigger re-provisioning of permission set to ALL_PROVISIONED_ACCOUNTS 56 | Topic Processor->Permission Set Sync processor: Post permission set sync notification 57 | note over Permission Set Sync processor: from this point, the solution\n follows same sequence as\n create operation 58 | else deltaDoesNotExist: 59 | Topic Processor->Topic Processor: log and complete the operation 60 | end 61 | else delete: 62 | Topic Processor->Solution Persistence(DynamoDB): fetch permissionSetArn value 63 | Topic Processor->AWS IAM Identity Center Admin API: delete permission set object 64 | Topic Processor->Solution Persistence(DynamoDB): delete permissionSetArn value 65 | end 66 | deactivate Topic Processor 67 | else invalid payload: 68 | User Interface Handler->Solution User: Return validation error 69 | end 70 | -------------------------------------------------------------------------------- /docs/wsd-code/sso-events-flow.wsd: -------------------------------------------------------------------------------- 1 | title SSO Events flow 2 | 3 | SSO events EventBridge Rule(SSO account)->SSO Events Processor: trigger matching SSO event 4 | SSO Events Processor->SSO Events Processor: determineEventType() 5 | alt Group creation event 6 | SSO Events Processor->SSO Events Processor: calculate displayName of the group based on received event 7 | note over SSO Events Processor: from this point, the same sequence as account\n assignment flow is followed 8 | else Group deletion event 9 | SSO Events Processor->SSO Events Processor: log and complete the operation 10 | else User creation event 11 | SSO Events Processor->AWS Identity Store: lookup userName from the userId in the event 12 | note over SSO Events Processor: from this point, the same sequence as account\n assignment flow is followed 13 | else User deletion event 14 | SSO Events Processor->SSO Events Processor: log and complete the operation 15 | end -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/test"], 3 | testMatch: ["**/*.test.ts"], 4 | transform: { 5 | "^.+\\.tsx?$": "ts-jest", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/build/buildConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build parameters inteface Used for validating configuration files at 3 | * synthesis time for correctness of data type and data ranges/values 4 | */ 5 | export interface BuildConfig { 6 | readonly App: string /** Used as prefix for resource and stack names */; 7 | readonly Environment: string /** Used as prefix for resource and stack names */; 8 | readonly Version: string /** Used for aligning with github version */; 9 | readonly PipelineSettings: PipelineSettings; 10 | readonly Parameters: Parameters; 11 | } 12 | 13 | /** Pipeline specific parameters */ 14 | export interface PipelineSettings { 15 | readonly BootstrapQualifier: string /** CDK bootstrap qualifier to deploy the solution */; 16 | readonly DeploymentAccountId: string; 17 | readonly DeploymentAccountRegion: string; 18 | readonly TargetAccountId: string; 19 | readonly TargetAccountRegion: string; 20 | readonly SSOServiceAccountId: string; 21 | readonly SSOServiceAccountRegion: string; 22 | readonly OrgMainAccountId: string; 23 | readonly RepoType: string /** Source repo type - accepted values are one of ["codecommit","codestar"] */; 24 | readonly RepoArn: string; 25 | /** 26 | * AWS CodeCommit source code repository ARN - only checked when RepoType is 27 | * set to codecommit 28 | */ 29 | readonly RepoBranchName: string; 30 | /** 31 | * AWS CodeCommit / CodeStar supported source code repository branch - checked 32 | * when RepoType is set to either codecommit or codestar 33 | */ 34 | readonly RepoName: string /** AWS CodeStar repo name - only checked when RepoType is set to codestar */; 35 | readonly CodeStarConnectionArn: string /** AWS CodeStar connection ARN - only checked when RepoType is set to codestar */; 36 | readonly SourceBucketName: string /** S3 bucket name - only checked when RepoType is set to s3 */; 37 | readonly SourceObjectKey: string /** S3 object key - only checked when RepoType is set to s3 */; 38 | readonly SynthCommand: string /** CDK synthesise command */; 39 | } 40 | 41 | /** Solution specific parameters */ 42 | export interface Parameters { 43 | readonly LinksProvisioningMode: string; 44 | /** 45 | * Account assignments provisioning mode - accepted values are one of ["api", 46 | * "s3"] 47 | */ 48 | readonly PermissionSetProvisioningMode: string /** Permission set provisioning mode - accepted values are one of ["api", "s3"] */; 49 | /** 50 | * IAM role arn created in target account with permissions to upload account 51 | * assignments to S3/API interfaces 52 | */ 53 | readonly LinkCallerRoleArn: string; 54 | /** 55 | * IAM role arn created in target account with permissions to upload 56 | * permission sets to S3/API interfaces 57 | */ 58 | readonly PermissionSetCallerRoleArn: string; 59 | readonly NotificationEmail: string /** Notification email used by solution to send error notifications etc */; 60 | readonly AccountAssignmentVisibilityTimeoutHours: number; 61 | /** 62 | * Visibility timeout parameter , used for scaling the solution in large 63 | * enterprises 64 | */ 65 | readonly IsAdUsed: boolean; 66 | readonly DomainName: string; 67 | /** 68 | * Used as switch to do a one-time import of all AWS IAM Identity Center 69 | * account assignments and permission sets into the solution 70 | */ 71 | readonly ImportCurrentSSOConfiguration: boolean; 72 | /** 73 | * Used as switch to do one-time format upgrade of all the account assignments 74 | * that the solution provisioned and persisted in DynamoDB 75 | */ 76 | readonly UpgradeFromVersionLessThanV303: boolean; 77 | /** 78 | * Used as switch to determine whether OU traversal is parent level only (or) 79 | * full tree traversal 80 | */ 81 | readonly SupportNestedOU: boolean; 82 | /** 83 | * Used as switch to set the level of lambda function logging the solution 84 | * should use 85 | */ 86 | readonly FunctionLogMode: string; 87 | } 88 | -------------------------------------------------------------------------------- /lib/build/regionSwitchBuildConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build parameters inteface Used for validating configuration files at 3 | * synthesis time for correctness of data type and data ranges/values 4 | */ 5 | 6 | export interface RegionSwitchBuildConfig { 7 | readonly BootstrapQualifier: string /** CDK bootstrap qualifier */; 8 | readonly SSOServiceAccountId: string; 9 | readonly SSOServiceAccountRegion: string; 10 | readonly SSOServiceTargetAccountRegion: string; 11 | } 12 | -------------------------------------------------------------------------------- /lib/constructs/cross-account-role.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom CDK construct that enables creating AWS IAM Roles that could be 3 | * assumed by cross-account prinicpals. This construct sets the policy document, 4 | * assume role account details as received in props and also creates an AWS SSM 5 | * parameter with cross-account read access (using SSMParamWriter construct) 6 | * with the roleArn value 7 | */ 8 | 9 | import { AccountPrincipal, PolicyStatement, Role } from "aws-cdk-lib/aws-iam"; 10 | import { Construct } from "constructs"; 11 | import { BuildConfig } from "../build/buildConfig"; 12 | import { SSMParamWriter } from "./ssm-param-writer"; 13 | import { name } from "./helpers"; 14 | 15 | export interface CrossAccountRoleProps { 16 | readonly assumeAccountID: string; 17 | readonly roleNameKey: string; 18 | readonly policyStatement: PolicyStatement; 19 | } 20 | 21 | export class CrossAccountRole extends Construct { 22 | public readonly role: Role; 23 | 24 | constructor( 25 | scope: Construct, 26 | id: string, 27 | buildConfig: BuildConfig, 28 | crossAccountRoleProps: CrossAccountRoleProps, 29 | ) { 30 | super(scope, id); 31 | 32 | /** 33 | * Create cross account role with trust policy set to the assuming account 34 | * ID principal 35 | */ 36 | this.role = new Role( 37 | this, 38 | name(buildConfig, `${crossAccountRoleProps.roleNameKey}-role`), 39 | { 40 | assumedBy: new AccountPrincipal(crossAccountRoleProps.assumeAccountID), 41 | }, 42 | ); 43 | 44 | /** 45 | * Add the required permissions passed in as part of the construct 46 | * initiation 47 | */ 48 | this.role.addToPrincipalPolicy(crossAccountRoleProps.policyStatement); 49 | 50 | /** 51 | * Write the roleArn parameter into SSM enabling the assuming account ID 52 | * with read permissions (through custom SSMParamWriter construct) 53 | */ 54 | new SSMParamWriter( 55 | this, 56 | name(buildConfig, `${crossAccountRoleProps.roleNameKey}-roleArn`), 57 | buildConfig, 58 | { 59 | ParamNameKey: `${crossAccountRoleProps.roleNameKey}-roleArn`, 60 | ParamValue: this.role.roleArn, 61 | ReaderAccountId: crossAccountRoleProps.assumeAccountID, 62 | }, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/constructs/helpers.ts: -------------------------------------------------------------------------------- 1 | /** All helper utilities used by different constructs */ 2 | 3 | import { BuildConfig } from "../build/buildConfig"; 4 | 5 | /** 6 | * Environment specific resource naming function 7 | * 8 | * @param buildConfig 9 | * @param resourcename 10 | * @returns Environment specific resource name 11 | */ 12 | export function name(buildConfig: BuildConfig, resourcename: string): string { 13 | return buildConfig.Environment + "-" + resourcename; 14 | } 15 | -------------------------------------------------------------------------------- /lib/constructs/import-artefacts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This construct allows read the non-deterministic values of resources created 3 | * in preSolutions stack. This is to avoid creating circular dependencies 4 | * between preSolutions and ssoImportArtefacts-part2 through the default CFN 5 | * exports. We circumvent that by relying on AWS SSM parameter store based 6 | * reads 7 | */ 8 | 9 | import { ITable, Table } from "aws-cdk-lib/aws-dynamodb"; 10 | import { IKey, Key } from "aws-cdk-lib/aws-kms"; 11 | import * as lambda from "aws-cdk-lib/aws-lambda"; 12 | import { ILayerVersion } from "aws-cdk-lib/aws-lambda"; 13 | import { Bucket, IBucket } from "aws-cdk-lib/aws-s3"; 14 | import { ITopic, Topic } from "aws-cdk-lib/aws-sns"; 15 | import { StringParameter } from "aws-cdk-lib/aws-ssm"; 16 | import { Construct } from "constructs"; 17 | import { BuildConfig } from "../build/buildConfig"; 18 | import { SSMParamReader } from "./ssm-param-reader"; 19 | import { name } from "./helpers"; 20 | 21 | export class ImportArtefacts extends Construct { 22 | public readonly nodeJsLayer: ILayerVersion; 23 | public readonly accountAssignmentImportTopic: ITopic; 24 | public readonly permissionSetImportTopic: ITopic; 25 | public readonly currentConfigSMInvokeRoleArn: string; 26 | public readonly currentConfigSMDescribeRoleArn: string; 27 | public readonly importedPermissionSetHandlerSSOAPIRoleArn: string; 28 | public readonly importedSsoArtefactsBucket: IBucket; 29 | public readonly importedPsTable: ITable; 30 | public readonly importedPsArnTable: ITable; 31 | public readonly importedLinksTable: ITable; 32 | public readonly importedProvisionedLinksTable: ITable; 33 | public readonly importedddbTablesKey: IKey; 34 | public readonly importCmpAndPbFunctionArn: string; 35 | 36 | constructor(scope: Construct, id: string, buildConfig: BuildConfig) { 37 | super(scope, id); 38 | 39 | this.nodeJsLayer = lambda.LayerVersion.fromLayerVersionArn( 40 | this, 41 | name(buildConfig, "importedNodeJsLayerVersion"), 42 | StringParameter.valueForStringParameter( 43 | this, 44 | name(buildConfig, "nodeJsLayerVersionArn"), 45 | ).toString(), 46 | ); 47 | 48 | this.importedddbTablesKey = Key.fromKeyArn( 49 | this, 50 | name(buildConfig, "importedDdbTablesKey"), 51 | StringParameter.valueForStringParameter( 52 | this, 53 | name(buildConfig, "ddbTablesKeyArn"), 54 | ), 55 | ); 56 | 57 | this.currentConfigSMInvokeRoleArn = new SSMParamReader( 58 | this, 59 | name(buildConfig, "ssoListRoleArnpr"), 60 | buildConfig, 61 | { 62 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 63 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 64 | ParamNameKey: "ssoList-ssoapi-roleArn", 65 | }, 66 | ).paramValue; 67 | 68 | this.currentConfigSMDescribeRoleArn = new SSMParamReader( 69 | this, 70 | name(buildConfig, "currentConfigSMDescribeRoleArnpr"), 71 | buildConfig, 72 | { 73 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 74 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 75 | ParamNameKey: "smDescribe-ssoapi-roleArn", 76 | }, 77 | ).paramValue; 78 | 79 | this.importedPermissionSetHandlerSSOAPIRoleArn = new SSMParamReader( 80 | this, 81 | name(buildConfig, "importedPermissionSetHandlerSSOAPIRoleArn"), 82 | buildConfig, 83 | { 84 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 85 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 86 | ParamNameKey: "permissionSetHandler-ssoapi-roleArn", 87 | }, 88 | ).paramValue; 89 | 90 | this.importCmpAndPbFunctionArn = new SSMParamReader( 91 | this, 92 | name(buildConfig, "importCmpAndPbFunctionArn"), 93 | buildConfig, 94 | { 95 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 96 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 97 | ParamNameKey: "importCmpAndPbArn", 98 | }, 99 | ).paramValue; 100 | 101 | this.accountAssignmentImportTopic = Topic.fromTopicArn( 102 | this, 103 | name(buildConfig, "accountAssignmentImportTopic"), 104 | new SSMParamReader( 105 | this, 106 | name(buildConfig, "accountAssignmentImportTopicArnReader"), 107 | buildConfig, 108 | { 109 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 110 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 111 | ParamNameKey: "accountAssignmentImportTopicArn", 112 | }, 113 | ).paramValue, 114 | ); 115 | 116 | this.permissionSetImportTopic = Topic.fromTopicArn( 117 | this, 118 | name(buildConfig, "permissionSetImportTopic"), 119 | new SSMParamReader( 120 | this, 121 | name(buildConfig, "permissionSetImportTopicArnReader"), 122 | buildConfig, 123 | { 124 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 125 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 126 | ParamNameKey: "permissionSetImportTopicArn", 127 | }, 128 | ).paramValue, 129 | ); 130 | 131 | this.importedSsoArtefactsBucket = Bucket.fromBucketName( 132 | this, 133 | name(buildConfig, "importedSsoArtefactsBucket"), 134 | StringParameter.valueForStringParameter( 135 | this, 136 | name(buildConfig, "ssoArtefactsBucketName"), 137 | ), 138 | ); 139 | 140 | this.importedPsTable = Table.fromTableAttributes( 141 | this, 142 | name(buildConfig, "importedPsTable"), 143 | { 144 | tableArn: StringParameter.valueForStringParameter( 145 | this, 146 | name(buildConfig, "permissionSetTableArn"), 147 | ), 148 | }, 149 | ); 150 | this.importedPsArnTable = Table.fromTableArn( 151 | this, 152 | name(buildConfig, "importedPsArnTable"), 153 | StringParameter.valueForStringParameter( 154 | this, 155 | name(buildConfig, "permissionSetArnTableArn"), 156 | ), 157 | ); 158 | this.importedLinksTable = Table.fromTableAttributes( 159 | this, 160 | name(buildConfig, "importedLinksTable"), 161 | { 162 | tableArn: StringParameter.valueForStringParameter( 163 | this, 164 | name(buildConfig, "linksTableArn"), 165 | ), 166 | globalIndexes: [ 167 | "awsEntityData", 168 | "principalName", 169 | "permissionSetName", 170 | "principalType", 171 | ], 172 | }, 173 | ); 174 | this.importedProvisionedLinksTable = Table.fromTableAttributes( 175 | this, 176 | name(buildConfig, "importedProvisionedLinksTable"), 177 | { 178 | tableArn: StringParameter.valueForStringParameter( 179 | this, 180 | name(buildConfig, "provisionedLinksTableArn"), 181 | ), 182 | globalIndexes: ["tagKeyLookUp"], 183 | }, 184 | ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /lib/constructs/lambda-layers.ts: -------------------------------------------------------------------------------- 1 | /** Lambda layers construct */ 2 | 3 | import { 4 | Architecture, 5 | Code, 6 | LayerVersion, 7 | Runtime, 8 | } from "aws-cdk-lib/aws-lambda"; 9 | import { StringParameter } from "aws-cdk-lib/aws-ssm"; 10 | import { Construct } from "constructs"; 11 | import { join } from "path"; 12 | import { BuildConfig } from "../build/buildConfig"; 13 | import { name } from "./helpers"; 14 | 15 | export class LambdaLayers extends Construct { 16 | public readonly nodeJsLayer: LayerVersion; 17 | 18 | constructor(scope: Construct, id: string, buildConfig: BuildConfig) { 19 | super(scope, id); 20 | 21 | this.nodeJsLayer = new LayerVersion( 22 | this, 23 | name(buildConfig, "nodeJsLayer"), 24 | { 25 | code: Code.fromAsset( 26 | join(__dirname, "../", "lambda-layers", "nodejs-layer"), 27 | ), 28 | compatibleRuntimes: [Runtime.NODEJS_20_X], 29 | compatibleArchitectures: [Architecture.ARM_64], 30 | }, 31 | ); 32 | 33 | new StringParameter(this, name(buildConfig, "nodeJsLayerVersionArn"), { 34 | parameterName: name(buildConfig, "nodeJsLayerVersionArn"), 35 | stringValue: this.nodeJsLayer.layerVersionArn, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/constructs/lambda-proxy-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Proxy API construct that sets up the required access for the API and lambda 3 | * handler as well as implementation of CORS override so that a lambda error is 4 | * gracefully handled by the proxy API 5 | */ 6 | 7 | import { 8 | AccessLogFormat, 9 | AuthorizationType, 10 | LambdaIntegration, 11 | LambdaRestApi, 12 | LogGroupLogDestination, 13 | } from "aws-cdk-lib/aws-apigateway"; 14 | import { Effect, IRole, PolicyStatement, Role } from "aws-cdk-lib/aws-iam"; 15 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 16 | import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs"; 17 | import { CfnOutput } from "aws-cdk-lib"; 18 | import { Construct } from "constructs"; 19 | import { BuildConfig } from "../build/buildConfig"; 20 | import { name } from "./helpers"; 21 | 22 | export interface LambdaProxyAPIProps { 23 | apiNameKey: string; 24 | apiResourceName: string; 25 | proxyfunction: NodejsFunction; 26 | apiCallerRoleArn: string; 27 | methodtype: string; 28 | apiEndPointReaderAccountID: string; 29 | } 30 | 31 | export class LambdaProxyAPI extends Construct { 32 | public readonly lambdaProxyAPILogGroup: LogGroup; 33 | public readonly lambdaProxyAPI: LambdaRestApi; 34 | public readonly lambdaProxyAPIRole: IRole; 35 | 36 | constructor( 37 | scope: Construct, 38 | id: string, 39 | buildConfig: BuildConfig, 40 | lambdaProxyAPIProps: LambdaProxyAPIProps, 41 | ) { 42 | super(scope, id); 43 | 44 | this.lambdaProxyAPILogGroup = new LogGroup( 45 | this, 46 | name(buildConfig, `${lambdaProxyAPIProps.apiNameKey}-logGroup`), 47 | { 48 | retention: RetentionDays.ONE_MONTH, 49 | }, 50 | ); 51 | 52 | this.lambdaProxyAPI = new LambdaRestApi( 53 | this, 54 | name(buildConfig, lambdaProxyAPIProps.apiNameKey), 55 | { 56 | handler: lambdaProxyAPIProps.proxyfunction, 57 | restApiName: name(buildConfig, lambdaProxyAPIProps.apiNameKey), 58 | proxy: false, 59 | deployOptions: { 60 | accessLogDestination: new LogGroupLogDestination( 61 | this.lambdaProxyAPILogGroup, 62 | ), 63 | accessLogFormat: AccessLogFormat.jsonWithStandardFields(), 64 | }, 65 | }, 66 | ); 67 | 68 | new CfnOutput( 69 | this, 70 | name(buildConfig, `${lambdaProxyAPIProps.apiNameKey}-endpointURL`), 71 | { 72 | exportName: name( 73 | buildConfig, 74 | `${lambdaProxyAPIProps.apiNameKey}-endpointURL`, 75 | ), 76 | value: this.lambdaProxyAPI.url, 77 | }, 78 | ); 79 | 80 | const lambdaproxyAPIResource = this.lambdaProxyAPI.root.addResource( 81 | lambdaProxyAPIProps.apiResourceName, 82 | ); 83 | 84 | this.lambdaProxyAPIRole = Role.fromRoleArn( 85 | this, 86 | name(buildConfig, "importedPermissionSetRole"), 87 | lambdaProxyAPIProps.apiCallerRoleArn, 88 | ); 89 | 90 | const lambdaProxyAPIIntegration = new LambdaIntegration( 91 | lambdaProxyAPIProps.proxyfunction, 92 | ); 93 | 94 | const lambdaProxyAPIMethod = lambdaproxyAPIResource.addMethod( 95 | lambdaProxyAPIProps.methodtype, 96 | lambdaProxyAPIIntegration, 97 | { 98 | authorizationType: AuthorizationType.IAM, 99 | }, 100 | ); 101 | 102 | this.lambdaProxyAPIRole.addToPrincipalPolicy( 103 | new PolicyStatement({ 104 | actions: ["execute-api:Invoke"], 105 | effect: Effect.ALLOW, 106 | resources: [lambdaProxyAPIMethod.methodArn], 107 | }), 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/constructs/org-events.ts: -------------------------------------------------------------------------------- 1 | /** Composite construct that sets up all resources for org events handling */ 2 | 3 | import { Duration } from "aws-cdk-lib"; 4 | import { Architecture, ILayerVersion, Runtime } from "aws-cdk-lib/aws-lambda"; 5 | import { SnsEventSource } from "aws-cdk-lib/aws-lambda-event-sources"; 6 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 7 | import { ITopic } from "aws-cdk-lib/aws-sns"; 8 | import { Construct } from "constructs"; 9 | import { join } from "path"; 10 | import { BuildConfig } from "../build/buildConfig"; 11 | import { name } from "./helpers"; 12 | 13 | export interface OrgEventsProps { 14 | readonly linksTableName: string; 15 | readonly permissionSetArnTableName: string; 16 | readonly linkQueueUrl: string; 17 | readonly errorNotificationsTopicArn: string; 18 | readonly nodeJsLayer: ILayerVersion; 19 | readonly orgEventsNotificationTopic: ITopic; 20 | readonly listInstancesSSOAPIRoleArn: string; 21 | readonly listGroupsIdentityStoreAPIRoleArn: string; 22 | readonly provisionedlinksTableName: string; 23 | readonly orgListParentsRoleArn: string; 24 | } 25 | 26 | export class OrgEvents extends Construct { 27 | public readonly orgEventsHandler: NodejsFunction; 28 | 29 | constructor( 30 | scope: Construct, 31 | id: string, 32 | buildConfig: BuildConfig, 33 | orgEventsProps: OrgEventsProps, 34 | ) { 35 | super(scope, id); 36 | 37 | this.orgEventsHandler = new NodejsFunction( 38 | this, 39 | name(buildConfig, "orgEventsHandler"), 40 | { 41 | runtime: Runtime.NODEJS_20_X, 42 | functionName: name(buildConfig, "orgEventsHandler"), 43 | architecture: Architecture.ARM_64, 44 | entry: join( 45 | __dirname, 46 | "../", 47 | "lambda-functions", 48 | "application-handlers", 49 | "src", 50 | "orgEvents.ts", 51 | ), 52 | bundling: { 53 | externalModules: [ 54 | "@aws-sdk/client-sns", 55 | "@aws-sdk/client-dynamodb", 56 | "@aws-sdk/client-identitystore", 57 | "@aws-sdk/client-sso-admin", 58 | "@aws-sdk/credential-providers", 59 | "@aws-sdk/lib-dynamodb", 60 | "@aws-sdk/client-organizations", 61 | "@aws-sdk/client-sqs", 62 | "uuid", 63 | ], 64 | minify: true, 65 | }, 66 | layers: [orgEventsProps.nodeJsLayer], 67 | environment: { 68 | linkQueueUrl: orgEventsProps.linkQueueUrl, 69 | permissionSetArnTable: orgEventsProps.permissionSetArnTableName, 70 | DdbTable: orgEventsProps.linksTableName, 71 | errorNotificationsTopicArn: orgEventsProps.errorNotificationsTopicArn, 72 | adUsed: String(buildConfig.Parameters.IsAdUsed), 73 | domainName: buildConfig.Parameters.DomainName, 74 | SSOAPIRoleArn: orgEventsProps.listInstancesSSOAPIRoleArn, 75 | ISAPIRoleArn: orgEventsProps.listGroupsIdentityStoreAPIRoleArn, 76 | ssoRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 77 | provisionedLinksTable: orgEventsProps.provisionedlinksTableName, 78 | supportNestedOU: String(buildConfig.Parameters.SupportNestedOU), 79 | orgListParentsRoleArn: orgEventsProps.orgListParentsRoleArn, 80 | functionLogMode: buildConfig.Parameters.FunctionLogMode, 81 | }, 82 | timeout: Duration.minutes(5), //aggressive timeout to accommodate for child OU's having many parents 83 | }, 84 | ); 85 | 86 | this.orgEventsHandler.addEventSource( 87 | new SnsEventSource(orgEventsProps.orgEventsNotificationTopic), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/constructs/ssm-param-reader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom cloudformation resource construct that allows cross account/cross 3 | * region read of SSM parameter value 4 | */ 5 | 6 | import { PolicyStatement } from "aws-cdk-lib/aws-iam"; 7 | import { 8 | AwsCustomResource, 9 | AwsCustomResourcePolicy, 10 | PhysicalResourceId, 11 | } from "aws-cdk-lib/custom-resources"; 12 | import { Construct } from "constructs"; 13 | import { BuildConfig } from "../build/buildConfig"; 14 | import { v4 as uuidv4 } from "uuid"; 15 | import { name } from "./helpers"; 16 | 17 | const generateRandomString = uuidv4().toString().split("-")[0]; 18 | 19 | export interface SSMParamReaderProps { 20 | readonly ParamNameKey: string; 21 | readonly ParamAccountId: string; 22 | readonly ParamRegion: string; 23 | } 24 | 25 | export class SSMParamReader extends Construct { 26 | public readonly paramValue: string; 27 | 28 | constructor( 29 | scope: Construct, 30 | id: string, 31 | buildConfig: BuildConfig, 32 | ssmParamReaderprops: SSMParamReaderProps, 33 | ) { 34 | super(scope, id); 35 | 36 | const fullParamName = name(buildConfig, ssmParamReaderprops.ParamNameKey); 37 | const paramReaderRole = `arn:aws:iam::${ssmParamReaderprops.ParamAccountId}:role/${fullParamName}-readerRole`; 38 | 39 | const assumeRolePolicy = new PolicyStatement({ 40 | resources: [paramReaderRole], 41 | actions: ["sts:AssumeRole"], 42 | }); 43 | 44 | const paramReadResource = new AwsCustomResource( 45 | this, 46 | name(buildConfig, `${id.slice(id.length - 5)}-paramReadResource`), 47 | { 48 | onUpdate: { 49 | service: "SSM", 50 | action: "getParameter", 51 | parameters: { 52 | Name: fullParamName, 53 | }, 54 | physicalResourceId: PhysicalResourceId.of(generateRandomString), 55 | region: ssmParamReaderprops.ParamRegion, 56 | assumedRoleArn: paramReaderRole, 57 | }, 58 | policy: AwsCustomResourcePolicy.fromStatements([assumeRolePolicy]), 59 | }, 60 | ); 61 | 62 | this.paramValue = paramReadResource 63 | .getResponseField("Parameter.Value") 64 | .toString(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/constructs/ssm-param-writer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom SSM writer construct that facilitates cross account/cross region 3 | * reading through the SSMParamReader construct 4 | */ 5 | 6 | import { AccountPrincipal, Role } from "aws-cdk-lib/aws-iam"; 7 | import { StringParameter } from "aws-cdk-lib/aws-ssm"; 8 | import { Construct } from "constructs"; 9 | import { BuildConfig } from "../build/buildConfig"; 10 | import { name } from "./helpers"; 11 | 12 | export interface SSMParamWriterProps { 13 | readonly ParamNameKey: string; 14 | readonly ParamValue: string; 15 | readonly ReaderAccountId: string; 16 | } 17 | 18 | export class SSMParamWriter extends Construct { 19 | public readonly parameter: StringParameter; 20 | public readonly parameterReaderRole: Role; 21 | 22 | constructor( 23 | scope: Construct, 24 | id: string, 25 | buildConfig: BuildConfig, 26 | ssmParamWriterProps: SSMParamWriterProps, 27 | ) { 28 | super(scope, id); 29 | 30 | this.parameter = new StringParameter( 31 | this, 32 | name(buildConfig, ssmParamWriterProps.ParamNameKey), 33 | { 34 | parameterName: name(buildConfig, ssmParamWriterProps.ParamNameKey), 35 | stringValue: ssmParamWriterProps.ParamValue, 36 | }, 37 | ); 38 | 39 | this.parameterReaderRole = new Role( 40 | this, 41 | name(buildConfig, `${ssmParamWriterProps.ParamNameKey}-readerRole`), 42 | { 43 | roleName: name( 44 | buildConfig, 45 | `${ssmParamWriterProps.ParamNameKey}-readerRole`, 46 | ), 47 | assumedBy: new AccountPrincipal(ssmParamWriterProps.ReaderAccountId), 48 | }, 49 | ); 50 | 51 | this.parameter.grantRead(this.parameterReaderRole); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/constructs/sso-group-processor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Composite construct that sets up all resources for SSO event life cycle 3 | * notifications 4 | */ 5 | 6 | import { Architecture, ILayerVersion, Runtime } from "aws-cdk-lib/aws-lambda"; 7 | import { SnsEventSource } from "aws-cdk-lib/aws-lambda-event-sources"; 8 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 9 | import { ITopic } from "aws-cdk-lib/aws-sns"; 10 | import { Construct } from "constructs"; 11 | import { join } from "path"; 12 | import { BuildConfig } from "../build/buildConfig"; 13 | import { name } from "./helpers"; 14 | 15 | export interface SSOGroupProcessorProps { 16 | readonly linksTableName: string; 17 | readonly permissionSetArnTableName: string; 18 | readonly linkQueueUrl: string; 19 | readonly errorNotificationsTopicArn: string; 20 | readonly nodeJsLayer: ILayerVersion; 21 | readonly ssoGroupEventNotificationsTopic: ITopic; 22 | readonly ssoUserEventNotificationsTopic: ITopic; 23 | readonly listInstancesSSOAPIRoleArn: string; 24 | readonly listGroupsIdentityStoreAPIRoleArn: string; 25 | readonly orgListSMRoleArn: string; 26 | readonly processTargetAccountSMTopic: ITopic; 27 | } 28 | 29 | export class SSOGroupProcessor extends Construct { 30 | public readonly ssoGroupHandler: NodejsFunction; 31 | public readonly ssoUserHandler: NodejsFunction; 32 | 33 | constructor( 34 | scope: Construct, 35 | id: string, 36 | buildConfig: BuildConfig, 37 | ssoGroupProcessorProps: SSOGroupProcessorProps, 38 | ) { 39 | super(scope, id); 40 | 41 | this.ssoGroupHandler = new NodejsFunction( 42 | this, 43 | name(buildConfig, "ssoGroupHandler"), 44 | { 45 | runtime: Runtime.NODEJS_20_X, 46 | functionName: name(buildConfig, "ssoGroupHandler"), 47 | architecture: Architecture.ARM_64, 48 | entry: join( 49 | __dirname, 50 | "../", 51 | "lambda-functions", 52 | "application-handlers", 53 | "src", 54 | "groupsCud.ts", 55 | ), 56 | bundling: { 57 | externalModules: [ 58 | "@aws-sdk/client-dynamodb", 59 | "@aws-sdk/lib-dynamodb", 60 | "@aws-sdk/client-sns", 61 | "@aws-sdk/client-sfn", 62 | "@aws-sdk/client-sso-admin", 63 | "@aws-sdk/credential-providers", 64 | "@aws-sdk/client-sqs", 65 | "uuid", 66 | ], 67 | minify: true, 68 | }, 69 | layers: [ssoGroupProcessorProps.nodeJsLayer], 70 | environment: { 71 | permissionarntable: ssoGroupProcessorProps.permissionSetArnTableName, 72 | linkstable: ssoGroupProcessorProps.linksTableName, 73 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", 74 | linkQueueUrl: ssoGroupProcessorProps.linkQueueUrl, 75 | errorNotificationsTopicArn: 76 | ssoGroupProcessorProps.errorNotificationsTopicArn, 77 | SSOAPIRoleArn: ssoGroupProcessorProps.listInstancesSSOAPIRoleArn, 78 | processTargetAccountSMTopicArn: 79 | ssoGroupProcessorProps.processTargetAccountSMTopic.topicArn, 80 | orgListSMRoleArn: ssoGroupProcessorProps.orgListSMRoleArn, 81 | processTargetAccountSMArn: `arn:aws:states:us-east-1:${buildConfig.PipelineSettings.OrgMainAccountId}:stateMachine:${buildConfig.Environment}-processTargetAccountSM`, 82 | ssoRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 83 | supportNestedOU: String(buildConfig.Parameters.SupportNestedOU), 84 | functionLogMode: buildConfig.Parameters.FunctionLogMode, 85 | }, 86 | }, 87 | ); 88 | 89 | this.ssoGroupHandler.addEventSource( 90 | new SnsEventSource( 91 | ssoGroupProcessorProps.ssoGroupEventNotificationsTopic, 92 | ), 93 | ); 94 | 95 | this.ssoUserHandler = new NodejsFunction( 96 | this, 97 | name(buildConfig, "ssoUserHandler"), 98 | { 99 | runtime: Runtime.NODEJS_20_X, 100 | functionName: name(buildConfig, "ssoUserHandler"), 101 | architecture: Architecture.ARM_64, 102 | entry: join( 103 | __dirname, 104 | "../", 105 | "lambda-functions", 106 | "application-handlers", 107 | "src", 108 | "usersCud.ts", 109 | ), 110 | bundling: { 111 | externalModules: [ 112 | "@aws-sdk/client-dynamodb", 113 | "@aws-sdk/lib-dynamodb", 114 | "@aws-sdk/client-sns", 115 | "@aws-sdk/client-sfn", 116 | "@aws-sdk/client-sso-admin", 117 | "@aws-sdk/client-identitystore", 118 | "@aws-sdk/credential-providers", 119 | "@aws-sdk/client-sqs", 120 | "@aws-sdk/credential-providers", 121 | "uuid", 122 | ], 123 | minify: true, 124 | }, 125 | layers: [ssoGroupProcessorProps.nodeJsLayer], 126 | environment: { 127 | permissionarntable: ssoGroupProcessorProps.permissionSetArnTableName, 128 | linkstable: ssoGroupProcessorProps.linksTableName, 129 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", 130 | linkQueueUrl: ssoGroupProcessorProps.linkQueueUrl, 131 | errorNotificationsTopicArn: 132 | ssoGroupProcessorProps.errorNotificationsTopicArn, 133 | SSOAPIRoleArn: ssoGroupProcessorProps.listInstancesSSOAPIRoleArn, 134 | ISAPIRoleArn: 135 | ssoGroupProcessorProps.listGroupsIdentityStoreAPIRoleArn, 136 | processTargetAccountSMTopicArn: 137 | ssoGroupProcessorProps.processTargetAccountSMTopic.topicArn, 138 | orgListSMRoleArn: ssoGroupProcessorProps.orgListSMRoleArn, 139 | processTargetAccountSMArn: `arn:aws:states:us-east-1:${buildConfig.PipelineSettings.OrgMainAccountId}:stateMachine:${buildConfig.Environment}-processTargetAccountSM`, 140 | ssoRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 141 | supportNestedOU: String(buildConfig.Parameters.SupportNestedOU), 142 | functionLogMode: buildConfig.Parameters.FunctionLogMode, 143 | }, 144 | }, 145 | ); 146 | 147 | this.ssoUserHandler.addEventSource( 148 | new SnsEventSource(ssoGroupProcessorProps.ssoUserEventNotificationsTopic), 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/constructs/utility.ts: -------------------------------------------------------------------------------- 1 | /** Utility construct in solution artefacts stack that allows shareable resources */ 2 | 3 | import { ITopic, Topic } from "aws-cdk-lib/aws-sns"; 4 | import { StringParameter } from "aws-cdk-lib/aws-ssm"; 5 | import { Construct } from "constructs"; 6 | import { BuildConfig } from "../build/buildConfig"; 7 | import { SSMParamReader } from "./ssm-param-reader"; 8 | import { name } from "./helpers"; 9 | 10 | export class Utility extends Construct { 11 | public readonly orgEventsNotificationsTopic: ITopic; 12 | public readonly ssoGroupEventsNotificationsTopic: ITopic; 13 | public readonly ssoUserEventsNotificationsTopic: ITopic; 14 | public readonly processTargetAccountSMTopic: ITopic; 15 | 16 | constructor(scope: Construct, id: string, buildConfig: BuildConfig) { 17 | super(scope, id); 18 | 19 | this.orgEventsNotificationsTopic = Topic.fromTopicArn( 20 | this, 21 | name(buildConfig, "orgEvenstNotificationsTopic"), 22 | new SSMParamReader( 23 | this, 24 | name(buildConfig, "orgEventsNotificationTopicArnReader"), 25 | buildConfig, 26 | { 27 | ParamAccountId: buildConfig.PipelineSettings.OrgMainAccountId, 28 | ParamRegion: "us-east-1", 29 | ParamNameKey: "orgEventsNotificationsTopicArn", 30 | }, 31 | ).paramValue, 32 | ); 33 | 34 | this.ssoGroupEventsNotificationsTopic = Topic.fromTopicArn( 35 | this, 36 | name(buildConfig, "ssoGroupEventsNotificationsTopic"), 37 | new SSMParamReader( 38 | this, 39 | name(buildConfig, "ssoGroupEventsNotificationTopicArnReader"), 40 | buildConfig, 41 | { 42 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 43 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 44 | ParamNameKey: "ssoGroupEventsNotificationTopicArn", 45 | }, 46 | ).paramValue, 47 | ); 48 | 49 | this.ssoUserEventsNotificationsTopic = Topic.fromTopicArn( 50 | this, 51 | name(buildConfig, "ssoUserEventsNotificationsTopic"), 52 | new SSMParamReader( 53 | this, 54 | name(buildConfig, "ssoUserEventsNotificationsTopicArnReader"), 55 | buildConfig, 56 | { 57 | ParamAccountId: buildConfig.PipelineSettings.SSOServiceAccountId, 58 | ParamRegion: buildConfig.PipelineSettings.SSOServiceAccountRegion, 59 | ParamNameKey: "ssoUserEventsNotificationTopicArn", 60 | }, 61 | ).paramValue, 62 | ); 63 | 64 | this.processTargetAccountSMTopic = Topic.fromTopicArn( 65 | this, 66 | name(buildConfig, "processTargetAccountSMTopic"), 67 | new SSMParamReader( 68 | this, 69 | name(buildConfig, "processTargetAccountSMTopicArnReader"), 70 | buildConfig, 71 | { 72 | ParamAccountId: buildConfig.PipelineSettings.OrgMainAccountId, 73 | ParamRegion: "us-east-1", 74 | ParamNameKey: "processTargetAccountSMTopicArn", 75 | }, 76 | ).paramValue, 77 | ); 78 | 79 | new StringParameter( 80 | this, 81 | name(buildConfig, "importedProcessTargetAccountSMTopicArn"), 82 | { 83 | parameterName: name( 84 | buildConfig, 85 | "importedProcessTargetAccountSMTopicArn", 86 | ), 87 | stringValue: this.processTargetAccountSMTopic.topicArn, 88 | }, 89 | ); 90 | 91 | new StringParameter( 92 | this, 93 | name(buildConfig, "ssoGroupEventNotificationsTopicArn"), 94 | { 95 | parameterName: name(buildConfig, "ssoGroupEventNotificationsTopicArn"), 96 | stringValue: this.ssoGroupEventsNotificationsTopic.topicArn, 97 | }, 98 | ); 99 | 100 | new StringParameter( 101 | this, 102 | name(buildConfig, "ssoUserEventsNotificationsTopicArn"), 103 | { 104 | parameterName: name(buildConfig, "ssoUserEventsNotificationsTopicArn"), 105 | stringValue: this.ssoUserEventsNotificationsTopic.topicArn, 106 | }, 107 | ); 108 | 109 | new StringParameter( 110 | this, 111 | name(buildConfig, "orgEventsNotificationsTopicArn"), 112 | { 113 | parameterName: name( 114 | buildConfig, 115 | "importedOrgEventsNotificationsTopicArn", 116 | ), 117 | stringValue: this.orgEventsNotificationsTopic.topicArn, 118 | }, 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/lambda-functions/application-handlers/src/managedPolicyQueueProcessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Objective: 3 | * 4 | * Processes managed policy queue items, by reading and posting the payload to 5 | * state machines for managed policy(AWS and customer) operations 6 | */ 7 | 8 | const { 9 | SSOMPRoleArn, 10 | errorNotificationsTopicArn, 11 | ssoRegion, 12 | AWS_REGION, 13 | functionLogMode, 14 | AWS_LAMBDA_FUNCTION_NAME, 15 | } = process.env; 16 | 17 | import { 18 | SFNClient, 19 | SFNServiceException, 20 | StartExecutionCommand, 21 | } from "@aws-sdk/client-sfn"; 22 | import { 23 | PublishCommand, 24 | SNSClient, 25 | SNSServiceException, 26 | } from "@aws-sdk/client-sns"; 27 | import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; 28 | 29 | import { SQSEvent } from "aws-lambda"; 30 | import { v4 as uuidv4 } from "uuid"; 31 | import { 32 | logModes, 33 | ManagedPolicyQueueObject, 34 | requestStatus, 35 | } from "../../helpers/src/interfaces"; 36 | import { 37 | constructExceptionMessage, 38 | constructExceptionMessageforLogger, 39 | logger, 40 | } from "../../helpers/src/utilities"; 41 | 42 | const sfnClientObject = new SFNClient({ 43 | region: ssoRegion, 44 | maxAttempts: 2, 45 | credentials: fromTemporaryCredentials({ 46 | params: { 47 | RoleArn: SSOMPRoleArn, 48 | }, 49 | }), 50 | }); 51 | 52 | const snsClientObject = new SNSClient({ region: AWS_REGION, maxAttempts: 2 }); 53 | 54 | const handlerName = AWS_LAMBDA_FUNCTION_NAME + ""; 55 | const messageSubject = "Exception in managed policy queue processor"; 56 | const mpName = ""; 57 | 58 | export const handler = async (event: SQSEvent) => { 59 | await Promise.all( 60 | event.Records.map(async (record) => { 61 | const requestId = uuidv4().toString(); 62 | logger( 63 | { 64 | handler: handlerName, 65 | logMode: logModes.Info, 66 | requestId: requestId, 67 | status: requestStatus.InProgress, 68 | statusMessage: `Started processing managed policy queue operation`, 69 | }, 70 | functionLogMode, 71 | ); 72 | try { 73 | const message: ManagedPolicyQueueObject = JSON.parse(record.body); 74 | if (message.managedPolicytype === "aws") { 75 | /** AWS managed policy operation */ 76 | await sfnClientObject.send( 77 | new StartExecutionCommand({ 78 | stateMachineArn: message.stateMachineArn, 79 | input: JSON.stringify(message.managedPolicyObject), 80 | }), 81 | ); 82 | logger( 83 | { 84 | handler: handlerName, 85 | logMode: logModes.Info, 86 | requestId: requestId, 87 | status: requestStatus.Completed, 88 | statusMessage: `Completed posting managed policy queue operation`, 89 | }, 90 | functionLogMode, 91 | ); 92 | } else if (message.managedPolicytype === "customer") { 93 | /** Customer managed policy operation */ 94 | await sfnClientObject.send( 95 | new StartExecutionCommand({ 96 | stateMachineArn: message.stateMachineArn, 97 | input: JSON.stringify(message.managedPolicyObject), 98 | }), 99 | ); 100 | logger( 101 | { 102 | handler: handlerName, 103 | logMode: logModes.Info, 104 | requestId: requestId, 105 | status: requestStatus.Completed, 106 | statusMessage: `Completed posting managed policy queue operation`, 107 | }, 108 | functionLogMode, 109 | ); 110 | } else { 111 | logger( 112 | { 113 | handler: handlerName, 114 | logMode: logModes.Exception, 115 | requestId: requestId, 116 | status: requestStatus.FailedWithException, 117 | statusMessage: `Managed Policy type ${message.managedPolicytype} is incorrect`, 118 | }, 119 | functionLogMode, 120 | ); 121 | } 122 | } catch (err) { 123 | if ( 124 | err instanceof SFNServiceException || 125 | err instanceof SNSServiceException 126 | ) { 127 | await snsClientObject.send( 128 | new PublishCommand({ 129 | TopicArn: errorNotificationsTopicArn, 130 | Subject: messageSubject, 131 | Message: constructExceptionMessage( 132 | requestId, 133 | handlerName, 134 | err.name, 135 | err.message, 136 | mpName, 137 | ), 138 | }), 139 | ); 140 | logger({ 141 | handler: handlerName, 142 | requestId: requestId, 143 | logMode: logModes.Exception, 144 | status: requestStatus.FailedWithException, 145 | statusMessage: constructExceptionMessageforLogger( 146 | requestId, 147 | err.name, 148 | err.message, 149 | mpName, 150 | ), 151 | }); 152 | } else { 153 | await snsClientObject.send( 154 | new PublishCommand({ 155 | TopicArn: errorNotificationsTopicArn, 156 | Subject: messageSubject, 157 | Message: constructExceptionMessage( 158 | requestId, 159 | handlerName, 160 | "Unhandled exception", 161 | JSON.stringify(err), 162 | mpName, 163 | ), 164 | }), 165 | ); 166 | logger({ 167 | handler: handlerName, 168 | requestId: requestId, 169 | logMode: logModes.Exception, 170 | status: requestStatus.FailedWithException, 171 | statusMessage: constructExceptionMessageforLogger( 172 | requestId, 173 | "Unhandled exception", 174 | JSON.stringify(err), 175 | mpName, 176 | ), 177 | }); 178 | } 179 | } 180 | }), 181 | ); 182 | }; 183 | -------------------------------------------------------------------------------- /lib/lambda-functions/application-handlers/src/processTargetAccountSMListener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Objective: Listener for step function outputs from org account Trigger 3 | * source: Process Target Account SM topic 4 | * 5 | * - Determines if entity_tye is account_tag and if it is, extracts the account id 6 | * - Prepares the payload required 7 | * - Posts the payload to link manager topic 8 | * - Catch all failures in a generic exception block and post the error details to 9 | * error notifications topics 10 | */ 11 | 12 | const { 13 | errorNotificationsTopicArn, 14 | linkQueueUrl, 15 | AWS_REGION, 16 | functionLogMode, 17 | AWS_LAMBDA_FUNCTION_NAME, 18 | } = process.env; 19 | 20 | import { 21 | PublishCommand, 22 | SNSClient, 23 | SNSServiceException, 24 | } from "@aws-sdk/client-sns"; 25 | import { 26 | SendMessageCommand, 27 | SQSClient, 28 | SQSServiceException, 29 | } from "@aws-sdk/client-sqs"; 30 | import { SNSEvent } from "aws-lambda"; 31 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 32 | import { 33 | constructExceptionMessage, 34 | constructExceptionMessageforLogger, 35 | logger, 36 | } from "../../helpers/src/utilities"; 37 | 38 | const snsClientObject = new SNSClient({ region: AWS_REGION, maxAttempts: 2 }); 39 | const sqsClientObject = new SQSClient({ region: AWS_REGION, maxAttempts: 2 }); 40 | 41 | const handlerName = AWS_LAMBDA_FUNCTION_NAME + ""; 42 | const messageSubject = 43 | "Exception in processing state machine invocations for non-account scope type assignments"; 44 | let requestIdValue = ""; 45 | let targetIdValue = ""; 46 | export const handler = async (event: SNSEvent) => { 47 | const message = JSON.parse(event.Records[0].Sns.Message); 48 | requestIdValue = message.sourceRequestId; 49 | try { 50 | let targetId = ""; 51 | let tagKeyValue = "none"; 52 | if (message.entityType === "account_tag") { 53 | targetId = message.pretargetId.split("/")[2]; 54 | tagKeyValue = `${message.tagKey}^${targetId}`; 55 | } else { 56 | targetId = message.pretargetId; 57 | } 58 | targetIdValue = `${message.action}-${targetId}-${message.principalType}-${message.principalId}`; 59 | logger( 60 | { 61 | handler: handlerName, 62 | logMode: logModes.Info, 63 | requestId: requestIdValue, 64 | relatedData: targetIdValue, 65 | status: requestStatus.InProgress, 66 | statusMessage: `Processing SQS payload post for account assignment operation - ${message.action}`, 67 | }, 68 | functionLogMode, 69 | ); 70 | 71 | await sqsClientObject.send( 72 | new SendMessageCommand({ 73 | QueueUrl: linkQueueUrl, 74 | MessageBody: JSON.stringify({ 75 | ssoParams: { 76 | InstanceArn: message.instanceArn, 77 | TargetType: message.targetType, 78 | PrincipalType: message.principalType, 79 | PrincipalId: message.principalId, 80 | PermissionSetArn: message.permissionSetArn, 81 | TargetId: targetId, 82 | }, 83 | actionType: message.action, 84 | entityType: message.entityType, 85 | tagKeyLookUp: tagKeyValue, 86 | sourceRequestId: message.sourceRequestId, 87 | }), 88 | MessageGroupId: targetId.slice(-1), 89 | }), 90 | ); 91 | logger( 92 | { 93 | handler: handlerName, 94 | logMode: logModes.Info, 95 | requestId: requestIdValue, 96 | relatedData: targetIdValue, 97 | status: requestStatus.InProgress, 98 | statusMessage: `Posted account assignment operation - ${message.action} for targetId ${targetId} and permissionSetArn ${message.permissionSetArn} and principalId ${message.principalId} and principalType ${message.principalType}`, 99 | }, 100 | functionLogMode, 101 | ); 102 | } catch (err) { 103 | if ( 104 | err instanceof SNSServiceException || 105 | err instanceof SQSServiceException 106 | ) { 107 | await snsClientObject.send( 108 | new PublishCommand({ 109 | TopicArn: errorNotificationsTopicArn, 110 | Subject: messageSubject, 111 | Message: constructExceptionMessage( 112 | requestIdValue, 113 | handlerName, 114 | err.name, 115 | err.message, 116 | targetIdValue, 117 | ), 118 | }), 119 | ); 120 | logger({ 121 | handler: handlerName, 122 | requestId: requestIdValue, 123 | logMode: logModes.Exception, 124 | status: requestStatus.FailedWithException, 125 | statusMessage: constructExceptionMessageforLogger( 126 | requestIdValue, 127 | err.name, 128 | err.message, 129 | targetIdValue, 130 | ), 131 | }); 132 | } else { 133 | await snsClientObject.send( 134 | new PublishCommand({ 135 | TopicArn: errorNotificationsTopicArn, 136 | Subject: messageSubject, 137 | Message: constructExceptionMessage( 138 | requestIdValue, 139 | handlerName, 140 | "Unhandled exception", 141 | JSON.stringify(err), 142 | targetIdValue, 143 | ), 144 | }), 145 | ); 146 | logger({ 147 | handler: handlerName, 148 | requestId: requestIdValue, 149 | logMode: logModes.Exception, 150 | status: requestStatus.FailedWithException, 151 | statusMessage: constructExceptionMessageforLogger( 152 | requestIdValue, 153 | "Unhandled exception", 154 | JSON.stringify(err), 155 | targetIdValue, 156 | ), 157 | }); 158 | } 159 | } 160 | }; 161 | -------------------------------------------------------------------------------- /lib/lambda-functions/current-config-handlers/src/import-customermanagedpolicies-permissionsboundary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Function that would read and update solution persistence with customer 3 | * managed policies and permissions boundary 4 | */ 5 | 6 | const { AWS_REGION, functionLogMode, AWS_LAMBDA_FUNCTION_NAME } = process.env; 7 | 8 | const ssoAdminClientObject = new SSOAdminClient({ 9 | region: AWS_REGION, 10 | maxAttempts: 2, 11 | }); 12 | 13 | import { 14 | GetPermissionsBoundaryForPermissionSetCommand, 15 | ListCustomerManagedPolicyReferencesInPermissionSetCommand, 16 | SSOAdminClient, 17 | SSOAdminServiceException, 18 | } from "@aws-sdk/client-sso-admin"; 19 | import { 20 | DescribeCmpAndPb, 21 | logModes, 22 | requestStatus, 23 | } from "../../helpers/src/interfaces"; 24 | import { 25 | constructExceptionMessageforLogger, 26 | logger, 27 | } from "../../helpers/src/utilities"; 28 | 29 | const handlerName = AWS_LAMBDA_FUNCTION_NAME + ""; 30 | const permissionSetName = ""; 31 | 32 | export const handler = async (event: DescribeCmpAndPb) => { 33 | logger( 34 | { 35 | handler: handlerName, 36 | logMode: logModes.Info, 37 | status: requestStatus.InProgress, 38 | statusMessage: `AWS IAM Identity Center customer managed policy import started for permissionSetArn ${event.permissionSetArn}`, 39 | }, 40 | functionLogMode, 41 | ); 42 | try { 43 | if (event.objectToDescribe === "customerManagedPolicy") { 44 | const result = await ssoAdminClientObject.send( 45 | new ListCustomerManagedPolicyReferencesInPermissionSetCommand({ 46 | InstanceArn: event.instanceArn, 47 | PermissionSetArn: event.permissionSetArn, 48 | MaxResults: 10, 49 | }), 50 | ); 51 | logger({ 52 | handler: handlerName, 53 | logMode: logModes.Info, 54 | status: requestStatus.Completed, 55 | statusMessage: `AWS IAM Identity Center customer managed policy import completed for permissionSetArn ${event.permissionSetArn}`, 56 | }); 57 | return { 58 | status: "true", 59 | result: result, 60 | }; 61 | } else if (event.objectToDescribe === "permissionsBoundary") { 62 | const result = await ssoAdminClientObject.send( 63 | new GetPermissionsBoundaryForPermissionSetCommand({ 64 | InstanceArn: event.instanceArn, 65 | PermissionSetArn: event.permissionSetArn, 66 | }), 67 | ); 68 | logger({ 69 | handler: handlerName, 70 | logMode: logModes.Info, 71 | status: requestStatus.Completed, 72 | statusMessage: `AWS IAM Identity Center permission boundary import completed for permissionSetArn ${event.permissionSetArn}`, 73 | }); 74 | return { 75 | status: "true", 76 | result: result, 77 | }; 78 | } else { 79 | logger({ 80 | handler: handlerName, 81 | logMode: logModes.Warn, 82 | status: requestStatus.FailedWithError, 83 | statusMessage: `AWS IAM Identity Center customer managed policy/permissions boundary import completed for permissionSetArn ${event.permissionSetArn}`, 84 | }); 85 | return { 86 | status: "false", 87 | statusReason: `Incorrect describeOp type specified`, 88 | }; 89 | } 90 | } catch (error) { 91 | if ( 92 | error instanceof SSOAdminServiceException && 93 | error.name.toLowerCase() === "resourcenotfoundexception" 94 | ) { 95 | logger({ 96 | handler: handlerName, 97 | logMode: logModes.Info, 98 | status: requestStatus.Completed, 99 | statusMessage: `AWS IAM Identity Center customer managed policy/permissions boundary import completed for permissionSetArn ${event.permissionSetArn}`, 100 | }); 101 | return { 102 | status: "true", 103 | result: {}, 104 | }; 105 | } else if ( 106 | error instanceof SSOAdminServiceException && 107 | !(error.name.toLowerCase() === "resourcenotfoundexception") 108 | ) { 109 | { 110 | logger({ 111 | handler: handlerName, 112 | 113 | logMode: logModes.Exception, 114 | status: requestStatus.FailedWithException, 115 | statusMessage: constructExceptionMessageforLogger( 116 | "", 117 | error.name, 118 | error.message, 119 | permissionSetName, 120 | ), 121 | }); 122 | return { 123 | status: "false", 124 | statusReason: `SSO service exception ${error.name}`, 125 | }; 126 | } 127 | } else { 128 | logger({ 129 | handler: handlerName, 130 | logMode: logModes.Exception, 131 | status: requestStatus.FailedWithException, 132 | statusMessage: constructExceptionMessageforLogger( 133 | "", 134 | "Unhandled exception", 135 | JSON.stringify(error), 136 | permissionSetName, 137 | ), 138 | }); 139 | return { 140 | status: "false", 141 | statusReason: `Unhandled exception`, 142 | }; 143 | } 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /lib/lambda-functions/current-config-handlers/src/trigger-parentSM.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * - 3 | * 4 | * Objective: Implement custom resource that invokes importCurrentConfigSM state 5 | * machine in SSO account Trigger source: Cloudformation custom resource 6 | * provider framework 7 | * 8 | * - Invoke the state machine witht the payload 9 | * - If the request type is delete, we don't do anything as this is a invoke type 10 | * custom resource 11 | */ 12 | 13 | // Lambda types import 14 | // SDK and third party client imports 15 | const { AWS_LAMBDA_FUNCTION_NAME } = process.env; 16 | import { 17 | SFNClient, 18 | StartExecutionCommand, 19 | SFNServiceException, 20 | } from "@aws-sdk/client-sfn"; 21 | import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; 22 | import { CloudFormationCustomResourceEvent } from "aws-lambda"; 23 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 24 | import { 25 | constructExceptionMessageforLogger, 26 | logger, 27 | } from "../../helpers/src/utilities"; 28 | import { v4 as uuidv4 } from "uuid"; 29 | const handlerName = AWS_LAMBDA_FUNCTION_NAME + ""; 30 | 31 | export const handler = async (event: CloudFormationCustomResourceEvent) => { 32 | const { importCurrentConfigSMArn } = event.ResourceProperties; 33 | const requestId = uuidv4().toString(); 34 | try { 35 | const { 36 | smInvokeRoleArn, 37 | importAccountAssignmentSMArn, 38 | importPermissionSetSMArn, 39 | accountAssignmentImportTopicArn, 40 | permissionSetImportTopicArn, 41 | temporaryPermissionSetTableName, 42 | importCmpAndPbArn, 43 | ssoRegion, 44 | } = event.ResourceProperties; 45 | const sfnClientObject = new SFNClient({ 46 | region: ssoRegion, 47 | credentials: fromTemporaryCredentials({ 48 | params: { 49 | RoleArn: smInvokeRoleArn, 50 | }, 51 | }), 52 | maxAttempts: 2, 53 | }); 54 | const stateMachineExecution = await sfnClientObject.send( 55 | new StartExecutionCommand({ 56 | stateMachineArn: importCurrentConfigSMArn, 57 | input: JSON.stringify({ 58 | temporaryPermissionSetTableName: temporaryPermissionSetTableName, 59 | importAccountAssignmentSMArn: importAccountAssignmentSMArn, 60 | accountAssignmentImportTopicArn: accountAssignmentImportTopicArn, 61 | importPermissionSetSMArn: importPermissionSetSMArn, 62 | permissionSetImportTopicArn: permissionSetImportTopicArn, 63 | PhysicalResourceId: importCurrentConfigSMArn, 64 | importCmpAndPbArn: importCmpAndPbArn, 65 | eventType: event.RequestType, 66 | triggerSource: "CloudFormation", 67 | requestId: requestId, 68 | waitSeconds: 2, 69 | pageSize: 5, 70 | }), 71 | }), 72 | ); 73 | logger({ 74 | handler: "parentInvokeSM", 75 | logMode: logModes.Info, 76 | relatedData: `${stateMachineExecution.executionArn}`, 77 | requestId: requestId, 78 | status: requestStatus.InProgress, 79 | statusMessage: `Custom resource creation triggered stateMachine`, 80 | }); 81 | //No status return as it would be updated by the state machine 82 | return { 83 | PhysicalResourceId: importCurrentConfigSMArn, 84 | stateMachineExecutionArn: stateMachineExecution.executionArn, 85 | requestId: requestId, 86 | }; 87 | } catch (err) { 88 | if (err instanceof SFNServiceException) { 89 | logger({ 90 | handler: handlerName, 91 | requestId: requestId, 92 | logMode: logModes.Exception, 93 | status: requestStatus.FailedWithException, 94 | statusMessage: constructExceptionMessageforLogger( 95 | requestId, 96 | err.name, 97 | err.message, 98 | "", 99 | ), 100 | }); 101 | return { 102 | Status: "FAILED", 103 | PhysicalResourceId: importCurrentConfigSMArn, 104 | Reason: constructExceptionMessageforLogger( 105 | requestId, 106 | err.name, 107 | err.message, 108 | "", 109 | ), 110 | }; 111 | } else { 112 | logger({ 113 | handler: handlerName, 114 | requestId: requestId, 115 | logMode: logModes.Exception, 116 | status: requestStatus.FailedWithException, 117 | statusMessage: constructExceptionMessageforLogger( 118 | requestId, 119 | "Unhandled exception", 120 | JSON.stringify(err), 121 | "", 122 | ), 123 | }); 124 | return { 125 | Status: "FAILED", 126 | PhysicalResourceId: importCurrentConfigSMArn, 127 | Reason: constructExceptionMessageforLogger( 128 | requestId, 129 | "Unhandled exception", 130 | JSON.stringify(err), 131 | "", 132 | ), 133 | }; 134 | } 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /lib/lambda-functions/current-config-handlers/src/update-custom-resource.ts: -------------------------------------------------------------------------------- 1 | /** Objective: Update cloudformation with custom resource status */ 2 | const { smDescribeRoleArn, ssoRegion, ssoAccountId, AWS_LAMBDA_FUNCTION_NAME } = 3 | process.env; 4 | 5 | // Lambda types import 6 | // SDK and third party client imports 7 | import { 8 | DescribeExecutionCommand, 9 | SFNClient, 10 | SFNServiceException, 11 | } from "@aws-sdk/client-sfn"; 12 | import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; 13 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 14 | import { 15 | constructExceptionMessageforLogger, 16 | logger, 17 | StateMachineError, 18 | } from "../../helpers/src/utilities"; 19 | 20 | const sfnClientObject = new SFNClient({ 21 | region: ssoRegion, 22 | credentials: fromTemporaryCredentials({ 23 | params: { 24 | RoleArn: smDescribeRoleArn, 25 | }, 26 | }), 27 | maxAttempts: 2, 28 | }); 29 | const handlerName = AWS_LAMBDA_FUNCTION_NAME + ""; 30 | /* eslint-disable @typescript-eslint/no-explicit-any */ 31 | export const handler = async (event: any) => { 32 | //event is of any type and not CloudFormationCustomResource as it does not allow state to be passed between onEvent and isComplete handlers 33 | const { stateMachineExecutionArn, requestId } = event; 34 | try { 35 | logger({ 36 | handler: "updateCustomResource", 37 | logMode: logModes.Info, 38 | relatedData: `${stateMachineExecutionArn}`, 39 | requestId: requestId, 40 | status: requestStatus.InProgress, 41 | statusMessage: `Custom resource update - stateMachine status inquiry start`, 42 | }); 43 | const stateMachineExecutionResult = await sfnClientObject.send( 44 | new DescribeExecutionCommand({ 45 | executionArn: stateMachineExecutionArn, 46 | }), 47 | ); 48 | 49 | switch (stateMachineExecutionResult.status) { 50 | case "RUNNING": { 51 | logger({ 52 | handler: "updateCustomResource", 53 | logMode: logModes.Info, 54 | requestId: requestId, 55 | relatedData: `${stateMachineExecutionArn}`, 56 | status: requestStatus.InProgress, 57 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} still running`, 58 | }); 59 | return { 60 | IsComplete: false, 61 | }; 62 | } 63 | case "SUCCEEDED": { 64 | logger({ 65 | handler: "updateCustomResource", 66 | logMode: logModes.Info, 67 | requestId: requestId, 68 | relatedData: `${stateMachineExecutionArn}`, 69 | status: requestStatus.Completed, 70 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} complete`, 71 | }); 72 | return { 73 | IsComplete: true, 74 | }; 75 | } 76 | case "FAILED": { 77 | logger({ 78 | handler: "updateCustomResource", 79 | logMode: logModes.Exception, 80 | requestId: requestId, 81 | relatedData: `${stateMachineExecutionArn}`, 82 | status: requestStatus.FailedWithError, 83 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with error. See error details in ${ssoAccountId} account, ${ssoRegion} region`, 84 | }); 85 | throw new StateMachineError({ 86 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with error. See error details in ${ssoAccountId} account, ${ssoRegion} region`, 87 | }); 88 | } 89 | case "TIMED_OUT": { 90 | logger({ 91 | handler: "updateCustomResource", 92 | logMode: logModes.Exception, 93 | requestId: requestId, 94 | relatedData: `${stateMachineExecutionArn}`, 95 | status: requestStatus.FailedWithError, 96 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} timed out. See timeout details in ${ssoAccountId} account, ${ssoRegion} region`, 97 | }); 98 | throw new StateMachineError({ 99 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} timed out. See timeout details in ${ssoAccountId} account, ${ssoRegion} region`, 100 | }); 101 | } 102 | default: { 103 | logger({ 104 | handler: "updateCustomResource", 105 | logMode: logModes.Exception, 106 | relatedData: `${stateMachineExecutionArn}`, 107 | status: requestStatus.FailedWithError, 108 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} reached an unknown status. See details in ${ssoAccountId} account, ${ssoRegion} region`, 109 | }); 110 | throw new StateMachineError({ 111 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} reached an unknown status. See details in ${ssoAccountId} account, ${ssoRegion} region`, 112 | }); 113 | } 114 | } 115 | } catch (err) { 116 | if (err instanceof StateMachineError) { 117 | throw err; 118 | } else if (err instanceof SFNServiceException) { 119 | logger({ 120 | handler: handlerName, 121 | requestId: requestId, 122 | logMode: logModes.Exception, 123 | status: requestStatus.FailedWithException, 124 | statusMessage: constructExceptionMessageforLogger( 125 | requestId, 126 | err.name, 127 | err.message, 128 | "", 129 | ), 130 | }); 131 | throw err; 132 | } else { 133 | logger({ 134 | handler: handlerName, 135 | requestId: requestId, 136 | logMode: logModes.Exception, 137 | status: requestStatus.FailedWithException, 138 | statusMessage: constructExceptionMessageforLogger( 139 | requestId, 140 | "Unhandled exception", 141 | JSON.stringify(err), 142 | "", 143 | ), 144 | }); 145 | throw err; 146 | } 147 | } 148 | }; 149 | -------------------------------------------------------------------------------- /lib/lambda-functions/custom-waiters/src/waitUntilAccountAssignmentCreation.ts: -------------------------------------------------------------------------------- 1 | /** Objective: Custom waiter for account assignment creation */ 2 | 3 | import { 4 | DescribeAccountAssignmentCreationStatusCommand, 5 | DescribeAccountAssignmentCreationStatusCommandInput, 6 | DescribeAccountAssignmentCreationStatusCommandOutput, 7 | SSOAdminClient, 8 | StatusValues, 9 | SSOAdminServiceException, 10 | } from "@aws-sdk/client-sso-admin"; 11 | import { 12 | checkExceptions, 13 | createWaiter, 14 | WaiterConfiguration, 15 | WaiterResult, 16 | WaiterState, 17 | } from "@aws-sdk/util-waiter"; 18 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 19 | import { logger } from "../../helpers/src/utilities"; 20 | 21 | const checkState = async ( 22 | client: SSOAdminClient, 23 | input: DescribeAccountAssignmentCreationStatusCommandInput, 24 | ): Promise => { 25 | let reason; 26 | try { 27 | const result: DescribeAccountAssignmentCreationStatusCommandOutput = 28 | await client.send( 29 | new DescribeAccountAssignmentCreationStatusCommand(input), 30 | ); 31 | reason = result; 32 | if ( 33 | result.AccountAssignmentCreationStatus?.Status === StatusValues.SUCCEEDED 34 | ) { 35 | return { state: WaiterState.SUCCESS, reason }; 36 | } else { 37 | return { state: WaiterState.RETRY, reason }; 38 | } 39 | } catch (exception) { 40 | if (exception instanceof SSOAdminServiceException) { 41 | reason = exception.message; 42 | return { state: WaiterState.FAILURE, reason }; 43 | } else { 44 | reason = exception; 45 | return { state: WaiterState.FAILURE, reason }; 46 | } 47 | } 48 | }; 49 | 50 | export const waitUntilAccountAssignmentCreation = async ( 51 | params: WaiterConfiguration, 52 | input: DescribeAccountAssignmentCreationStatusCommandInput, 53 | requestId: string, 54 | functionLogMode: string, 55 | ): Promise => { 56 | logger( 57 | { 58 | handler: "accountAssignmentCreationWaiter", 59 | logMode: logModes.Debug, 60 | requestId: requestId, 61 | relatedData: `${input.AccountAssignmentCreationRequestId}`, 62 | status: requestStatus.InProgress, 63 | statusMessage: `Setting service defaults`, 64 | }, 65 | functionLogMode, 66 | ); 67 | const serviceDefaults = { minDelay: 60, maxDelay: 120 }; 68 | logger( 69 | { 70 | handler: "accountAssignmentCreationWaiter", 71 | logMode: logModes.Info, 72 | requestId: requestId, 73 | relatedData: `${input.AccountAssignmentCreationRequestId}`, 74 | status: requestStatus.InProgress, 75 | statusMessage: `Invoking waiter for createAccountAssignment operation`, 76 | }, 77 | functionLogMode, 78 | ); 79 | const result = await createWaiter( 80 | { ...serviceDefaults, ...params }, 81 | input, 82 | checkState, 83 | ); 84 | logger( 85 | { 86 | handler: "accountAssignmentCreationWaiter", 87 | logMode: logModes.Info, 88 | requestId: requestId, 89 | relatedData: `${input.AccountAssignmentCreationRequestId}`, 90 | status: requestStatus.InProgress, 91 | statusMessage: `Waiter completed with result: ${JSON.stringify(result)}`, 92 | }, 93 | functionLogMode, 94 | ); 95 | return checkExceptions(result); 96 | }; 97 | -------------------------------------------------------------------------------- /lib/lambda-functions/custom-waiters/src/waitUntilAccountAssignmentDeletion.ts: -------------------------------------------------------------------------------- 1 | /** Objective: Custom waiter for account assignment deletion */ 2 | import { 3 | DescribeAccountAssignmentDeletionStatusCommand, 4 | DescribeAccountAssignmentDeletionStatusCommandInput, 5 | DescribeAccountAssignmentDeletionStatusCommandOutput, 6 | SSOAdminClient, 7 | StatusValues, 8 | SSOAdminServiceException, 9 | } from "@aws-sdk/client-sso-admin"; 10 | import { 11 | checkExceptions, 12 | createWaiter, 13 | WaiterConfiguration, 14 | WaiterResult, 15 | WaiterState, 16 | } from "@aws-sdk/util-waiter"; 17 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 18 | import { logger } from "../../helpers/src/utilities"; 19 | 20 | const checkState = async ( 21 | client: SSOAdminClient, 22 | input: DescribeAccountAssignmentDeletionStatusCommandInput, 23 | ): Promise => { 24 | let reason; 25 | try { 26 | const result: DescribeAccountAssignmentDeletionStatusCommandOutput = 27 | await client.send( 28 | new DescribeAccountAssignmentDeletionStatusCommand(input), 29 | ); 30 | reason = result; 31 | if ( 32 | result.AccountAssignmentDeletionStatus?.Status === StatusValues.SUCCEEDED 33 | ) { 34 | return { state: WaiterState.SUCCESS, reason }; 35 | } else { 36 | return { state: WaiterState.RETRY, reason }; 37 | } 38 | } catch (exception) { 39 | if (exception instanceof SSOAdminServiceException) { 40 | reason = exception.message; 41 | return { state: WaiterState.FAILURE, reason }; 42 | } else { 43 | reason = exception; 44 | return { state: WaiterState.FAILURE, reason }; 45 | } 46 | } 47 | }; 48 | 49 | export const waitUntilAccountAssignmentDeletion = async ( 50 | params: WaiterConfiguration, 51 | input: DescribeAccountAssignmentDeletionStatusCommandInput, 52 | requestId: string, 53 | functionLogMode: string, 54 | ): Promise => { 55 | logger( 56 | { 57 | handler: "accountAssignmentDeletionWaiter", 58 | logMode: logModes.Debug, 59 | requestId: requestId, 60 | relatedData: `${input.AccountAssignmentDeletionRequestId}`, 61 | status: requestStatus.InProgress, 62 | statusMessage: `Setting service defaults`, 63 | }, 64 | functionLogMode, 65 | ); 66 | const serviceDefaults = { minDelay: 60, maxDelay: 120 }; 67 | logger( 68 | { 69 | handler: "accountAssignmentDeletionWaiter", 70 | logMode: logModes.Info, 71 | requestId: requestId, 72 | relatedData: `${input.AccountAssignmentDeletionRequestId}`, 73 | status: requestStatus.InProgress, 74 | statusMessage: `Invoking waiter for deleteAccountAssignment operation`, 75 | }, 76 | functionLogMode, 77 | ); 78 | const result = await createWaiter( 79 | { ...serviceDefaults, ...params }, 80 | input, 81 | checkState, 82 | ); 83 | logger( 84 | { 85 | handler: "accountAssignmentDeletionWaiter", 86 | logMode: logModes.Info, 87 | requestId: requestId, 88 | relatedData: `${input.AccountAssignmentDeletionRequestId}`, 89 | status: requestStatus.InProgress, 90 | statusMessage: `Waiter completed with result: ${JSON.stringify(result)}`, 91 | }, 92 | functionLogMode, 93 | ); 94 | return checkExceptions(result); 95 | }; 96 | -------------------------------------------------------------------------------- /lib/lambda-functions/custom-waiters/src/waitUntilPermissionSetProvisioned.ts: -------------------------------------------------------------------------------- 1 | /** Objective: Custom waiter for permission set provisioning status */ 2 | import { 3 | DescribePermissionSetProvisioningStatusCommand, 4 | DescribePermissionSetProvisioningStatusCommandInput, 5 | DescribePermissionSetProvisioningStatusCommandOutput, 6 | SSOAdminClient, 7 | StatusValues, 8 | } from "@aws-sdk/client-sso-admin"; 9 | import { 10 | checkExceptions, 11 | createWaiter, 12 | WaiterConfiguration, 13 | WaiterResult, 14 | WaiterState, 15 | } from "@aws-sdk/util-waiter"; 16 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 17 | import { logger } from "../../helpers/src/utilities"; 18 | 19 | const checkState = async ( 20 | client: SSOAdminClient, 21 | input: DescribePermissionSetProvisioningStatusCommandInput, 22 | ): Promise => { 23 | let reason; 24 | try { 25 | const result: DescribePermissionSetProvisioningStatusCommandOutput = 26 | await client.send( 27 | new DescribePermissionSetProvisioningStatusCommand(input), 28 | ); 29 | reason = result; 30 | if ( 31 | result.PermissionSetProvisioningStatus?.Status === StatusValues.SUCCEEDED 32 | ) { 33 | return { state: WaiterState.SUCCESS, reason }; 34 | } else { 35 | return { state: WaiterState.RETRY, reason }; 36 | } 37 | } catch (exception) { 38 | reason = exception; 39 | return { state: WaiterState.FAILURE, reason }; 40 | } 41 | }; 42 | 43 | export const waitUntilPermissionSetProvisioned = async ( 44 | params: WaiterConfiguration, 45 | input: DescribePermissionSetProvisioningStatusCommandInput, 46 | requestId: string, 47 | ): Promise => { 48 | logger({ 49 | handler: "permissionSetProvisioningWaiter", 50 | logMode: logModes.Info, 51 | relatedData: `${input.ProvisionPermissionSetRequestId}`, 52 | requestId: requestId, 53 | status: requestStatus.InProgress, 54 | statusMessage: `Waiter invoked for permissionSetProvisioned Operation`, 55 | }); 56 | const serviceDefaults = { minDelay: 60, maxDelay: 120 }; 57 | const result = await createWaiter( 58 | { ...serviceDefaults, ...params }, 59 | input, 60 | checkState, 61 | ); 62 | logger({ 63 | handler: "permissionSetProvisioningWaiter", 64 | logMode: logModes.Info, 65 | relatedData: `${input.ProvisionPermissionSetRequestId}`, 66 | requestId: requestId, 67 | status: requestStatus.Completed, 68 | statusMessage: `Waiter Completed with result: ${JSON.stringify(result)}`, 69 | }); 70 | return checkExceptions(result); 71 | }; 72 | -------------------------------------------------------------------------------- /lib/lambda-functions/helpers/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface LinkPayload { 2 | readonly linkData: string; 3 | readonly action: string; 4 | } 5 | 6 | export interface LinkS3Payload { 7 | readonly linkData: string; 8 | } 9 | export interface Tag { 10 | readonly Key: string; 11 | readonly Value: string; 12 | } 13 | 14 | export interface CustomerManagedPolicyObject { 15 | readonly Name: string; 16 | readonly Path?: string; 17 | } 18 | export interface CreateUpdatePermissionSetDataProps { 19 | readonly permissionSetName: string; 20 | readonly description?: string; 21 | readonly sessionDurationInMinutes: string; 22 | readonly relayState: string; 23 | readonly tags: Array; 24 | readonly managedPoliciesArnList: Array; 25 | readonly customerManagedPoliciesList: Array; 26 | readonly inlinePolicyDocument: Record; 27 | } 28 | export interface CreateUpdatePermissionSetPayload { 29 | readonly action: string; 30 | readonly permissionSetData: CreateUpdatePermissionSetDataProps; 31 | } 32 | 33 | export interface DeletePermissionSetDataProps { 34 | readonly permissionSetName: string; 35 | } 36 | 37 | export interface DeletePermissionSetPayload { 38 | readonly action: string; 39 | readonly permissionSetData: DeletePermissionSetDataProps; 40 | } 41 | 42 | export interface ErrorMessage { 43 | readonly eventDetail?: string; 44 | readonly errorDetails?: string; 45 | readonly Subject?: string; 46 | } 47 | 48 | export interface ErrorNotificationPayload { 49 | readonly TopicArn?: string; 50 | readonly Message: string; 51 | } 52 | 53 | export interface LinkData { 54 | readonly awsEntityId: string; 55 | readonly awsEntityType: string; 56 | readonly awsEntityData: string; 57 | readonly permissionSetName: string; 58 | readonly principalName: string; 59 | readonly principalType: string; 60 | } 61 | 62 | export interface StateMachinePayload { 63 | readonly entityType: string; 64 | readonly action: string; 65 | readonly topicArn: string; 66 | readonly instanceArn: string; 67 | readonly targetType: string; 68 | readonly principalType: string; 69 | readonly permissionSetArn: string; 70 | readonly principalId: string; 71 | readonly sourceRequestId: string; 72 | readonly pageSize: number; 73 | readonly waitSeconds: number; 74 | readonly supportNestedOU: string; 75 | } 76 | 77 | export interface StaticSSOPayload { 78 | readonly InstanceArn: string; 79 | readonly TargetType: string; 80 | readonly PrincipalType: string; 81 | } 82 | 83 | export enum requestStatus { 84 | InProgress = "InProgress", 85 | Completed = "Completed", 86 | FailedWithError = "FailedWithError", 87 | FailedWithException = "FailedWithException", 88 | OnHold = "OnHold", 89 | Aborted = "Aborted", 90 | } 91 | 92 | export enum logModes { 93 | Exception = "Exception", 94 | Info = "Info", 95 | Debug = "Debug", 96 | Warn = "Warn", 97 | } 98 | 99 | export interface LogMessage { 100 | readonly logMode: logModes; 101 | readonly handler: string; 102 | readonly requestId?: string; 103 | readonly status: requestStatus; 104 | readonly statusMessage?: string; 105 | readonly relatedData?: string; 106 | readonly hasRelatedRequests?: boolean; 107 | readonly sourceRequestId?: string; 108 | } 109 | 110 | export interface CustomerManagedPolicyObjectOp { 111 | readonly operation: "attach" | "detach" | "describe"; 112 | readonly parentOperation?: "attach" | "detach"; 113 | readonly customerManagedPolicy: CustomerManagedPolicyObject; 114 | readonly permissionSetArn: string; 115 | readonly instanceArn: string; 116 | } 117 | 118 | export interface DescribeCmpAndPb { 119 | readonly objectToDescribe: "customerManagedPolicy" | "permissionsBoundary"; 120 | readonly instanceArn: string; 121 | readonly permissionSetArn: string; 122 | } 123 | 124 | export interface ManagedPolicyObjectOp { 125 | readonly operation: "attach" | "detach" | "describe"; 126 | readonly parentOperation?: "attach" | "detach"; 127 | readonly managedPolicyArn: string; 128 | readonly permissionSetArn: string; 129 | readonly instanceArn: string; 130 | } 131 | 132 | export interface DescribeOpIterator { 133 | readonly iterator: { 134 | readonly count: number; 135 | readonly index: number; 136 | readonly step: number; 137 | }; 138 | } 139 | 140 | export interface CustomerManagedPolicySFN { 141 | readonly instanceArn: string; 142 | readonly permissionSetArn: string; 143 | readonly waitSeconds: string; 144 | readonly operation: "attach" | "detach"; 145 | readonly processOpArn: string; 146 | readonly iteratorArn: string; 147 | readonly customerManagedPoliciesList: Array; 148 | } 149 | 150 | export interface ManagedPolicySFN { 151 | readonly instanceArn: string; 152 | readonly permissionSetArn: string; 153 | readonly waitSeconds: string; 154 | readonly operation: "attach" | "detach"; 155 | readonly processOpArn: string; 156 | readonly iteratorArn: string; 157 | readonly managedPoliciesArnList: Array; 158 | } 159 | 160 | export interface ManagedPolicyQueueObject { 161 | readonly managedPolicytype: "aws" | "customer"; 162 | readonly stateMachineArn: string; 163 | readonly managedPolicyObject: CustomerManagedPolicySFN | ManagedPolicySFN; 164 | } 165 | -------------------------------------------------------------------------------- /lib/lambda-functions/helpers/src/isoDurationUtility.ts: -------------------------------------------------------------------------------- 1 | interface DurationValues { 2 | years?: number; 3 | months?: number; 4 | weeks?: number; 5 | days?: number; 6 | hours?: number; 7 | minutes?: number; 8 | seconds?: number; 9 | } 10 | 11 | export type Duration = { 12 | negative?: boolean; 13 | } & DurationValues; 14 | 15 | const units: Array<{ unit: keyof DurationValues; symbol: string }> = [ 16 | { unit: "years", symbol: "Y" }, 17 | { unit: "months", symbol: "M" }, 18 | { unit: "weeks", symbol: "W" }, 19 | { unit: "days", symbol: "D" }, 20 | { unit: "hours", symbol: "H" }, 21 | { unit: "minutes", symbol: "M" }, 22 | { unit: "seconds", symbol: "S" }, 23 | ]; 24 | 25 | // Construction of the duration regex 26 | const r = (name: string, unit: string): string => 27 | `((?<${name}>-?\\d*[\\.,]?\\d+)${unit})?`; 28 | // Eslint disable rule in place because while the regexp is dynamic it's not long lasting, nor is it DoS prone as the Duration value is set in the calling method and is always static to reflect { minutes : } 29 | /* eslint-disable security/detect-non-literal-regexp */ 30 | const durationRegex = new RegExp( 31 | [ 32 | "(?-)?P", 33 | r("years", "Y"), 34 | r("months", "M"), 35 | r("weeks", "W"), 36 | r("days", "D"), 37 | "(T", 38 | r("hours", "H"), 39 | r("minutes", "M"), 40 | r("seconds", "S"), 41 | ")?", // end optional time 42 | ].join(""), 43 | ); 44 | 45 | function parseNum(stringValue: string): number | undefined { 46 | if (stringValue === "" || stringValue === undefined || stringValue === null) { 47 | return undefined; 48 | } 49 | 50 | return parseFloat(stringValue.replace(",", ".")); 51 | } 52 | 53 | export const InvalidDurationError = new Error("Invalid duration"); 54 | 55 | export function parseISODurationString(durationStr: string): Duration { 56 | const match = durationRegex.exec(durationStr); 57 | if (!match || !match.groups) { 58 | throw InvalidDurationError; 59 | } 60 | 61 | let empty = true; 62 | const values: DurationValues = {}; 63 | for (const { unit } of units) { 64 | if (Object.prototype.hasOwnProperty.call(match.groups, unit)) { 65 | empty = false; 66 | Object.assign(values, { 67 | // Eslint disable rule in place because the user input has already been validated earlier and also in line 62 , validation that match.groups has the unit property is done 68 | /* eslint-disable security/detect-object-injection */ 69 | [unit]: parseNum(match.groups[unit]), 70 | }); 71 | } 72 | } 73 | 74 | if (empty) { 75 | throw InvalidDurationError; 76 | } 77 | 78 | const duration: Duration = values; 79 | if (match.groups.negative) { 80 | duration.negative = true; 81 | } 82 | return duration; 83 | } 84 | 85 | const s = ( 86 | number: number | undefined, 87 | component: string, 88 | ): string | undefined => { 89 | if (!number) { 90 | return undefined; 91 | } 92 | 93 | let numberAsString = number.toString(); 94 | const exponentIndex = numberAsString.indexOf("e"); 95 | if (exponentIndex > -1) { 96 | const magnitude = parseInt(numberAsString.slice(exponentIndex + 2), 10); 97 | numberAsString = number.toFixed(magnitude + exponentIndex - 2); 98 | } 99 | 100 | return numberAsString + component; 101 | }; 102 | 103 | export function serializeDurationToISOFormat(duration: Duration): string { 104 | if ( 105 | !duration.years && 106 | !duration.months && 107 | !duration.weeks && 108 | !duration.days && 109 | !duration.hours && 110 | !duration.minutes && 111 | !duration.seconds 112 | ) { 113 | return "PT0S"; 114 | } 115 | 116 | return [ 117 | duration.negative && "-", 118 | "P", 119 | s(duration.years, "Y"), 120 | s(duration.months, "M"), 121 | s(duration.weeks, "W"), 122 | s(duration.days, "D"), 123 | (duration.hours || duration.minutes || duration.seconds) && "T", 124 | s(duration.hours, "H"), 125 | s(duration.minutes, "M"), 126 | s(duration.seconds, "S"), 127 | ] 128 | .filter(Boolean) 129 | .join(""); 130 | } 131 | 132 | export function convertDurationToMinutes(duration: Duration): number { 133 | // Compute provided date 134 | const then = new Date(Date.now()); 135 | if (duration.years) { 136 | then.setFullYear(then.getFullYear() + duration.years); 137 | } 138 | if (duration.months) { 139 | then.setMonth(then.getMonth() + duration.months); 140 | } 141 | if (duration.days) { 142 | then.setDate(then.getDate() + duration.days); 143 | } 144 | if (duration.hours) { 145 | then.setHours(then.getHours() + duration.hours); 146 | } 147 | if (duration.minutes) { 148 | then.setMinutes(then.getMinutes() + duration.minutes); 149 | } 150 | if (duration.seconds) { 151 | then.setMilliseconds(then.getMilliseconds() + duration.seconds * 1000); 152 | } 153 | // Special case weeks 154 | if (duration.weeks) { 155 | then.setDate(then.getDate() + duration.weeks * 7); 156 | } 157 | 158 | const now = new Date(Date.now()); 159 | 160 | return Math.ceil((then.getTime() - now.getTime()) / 60000); 161 | } 162 | 163 | export function getMinutesFromISODurationString(durationStr: string): string { 164 | const durationValue = parseISODurationString(durationStr); 165 | const minutesValue = convertDurationToMinutes(durationValue); 166 | return minutesValue.toString(); 167 | } 168 | -------------------------------------------------------------------------------- /lib/lambda-functions/helpers/src/payload-validator.ts: -------------------------------------------------------------------------------- 1 | import { ValidateFunction } from "ajv"; 2 | 3 | export class JSONParserError extends Error { 4 | constructor(public errors: { errorCode: string; message?: string }[]) { 5 | super(); 6 | } 7 | } 8 | 9 | export const imperativeParseJSON = ( 10 | data: object | string | null, 11 | validate: ValidateFunction, 12 | ): T => { 13 | if (!data) { 14 | throw new JSONParserError([{ errorCode: "null_json" }]); 15 | } 16 | 17 | try { 18 | const parsed = typeof data === "string" ? JSON.parse(data) : data; 19 | if (validate(parsed)) { 20 | return parsed as T; 21 | } 22 | } catch (e) { 23 | throw new JSONParserError([{ errorCode: "malformed_json" }]); 24 | } 25 | 26 | throw new JSONParserError( 27 | validate.errors!.map(({ instancePath, params }) => ({ 28 | errorCode: `pattern-error`, 29 | message: `Failure on property ${instancePath} . Schema for property should match pattern ${params.pattern}`, 30 | })), 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/lambda-functions/managed-policy-handlers/src/describeOpIterator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility function ensures a describe call on a customer managed policy is only 3 | * run X number of times 4 | */ 5 | 6 | import { DescribeOpIterator } from "../../helpers/src/interfaces"; 7 | 8 | export const handler = async (event: DescribeOpIterator) => { 9 | let index = event.iterator.index; 10 | const count = event.iterator.count; 11 | const step = event.iterator.step; 12 | 13 | index = index + step; 14 | 15 | return { 16 | index: index, 17 | step: step, 18 | count: count, 19 | continue: index < count, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/lambda-functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-sso-extensions-for-enterprise-layer", 3 | "version": "3.1.9", 4 | "description": "AWS SSO Permissions Utility Layer", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "dependencies": { 10 | "@aws-sdk/client-dynamodb": "^3.172.0", 11 | "@aws-sdk/client-identitystore": "^3.171.0", 12 | "@aws-sdk/client-organizations": "^3.171.0", 13 | "@aws-sdk/client-s3": "^3.171.0", 14 | "@aws-sdk/client-sfn": "^3.171.0", 15 | "@aws-sdk/client-sns": "^3.171.0", 16 | "@aws-sdk/client-sqs": "^3.171.0", 17 | "@aws-sdk/client-ssm": "^3.171.0", 18 | "@aws-sdk/client-sso-admin": "^3.171.0", 19 | "@aws-sdk/credential-providers": "^3.171.0", 20 | "@aws-sdk/lib-dynamodb": "^3.172.0", 21 | "@aws-sdk/util-dynamodb": "^3.172.0", 22 | "@aws-sdk/util-waiter": "^3.171.0", 23 | "ajv": "^8.11.0", 24 | "json-diff": "^0.9.0", 25 | "uuid": "^9.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/json-diff": "^0.9.0", 29 | "@types/uuid": "^8.3.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/lambda-functions/region-switch/src/rs-create-permission-sets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates permission sets in AWS IAM Identity Center. Triggered by Region 3 | * switch deploy state machine with payload containing instance arn, identity 4 | * store Id and permission set object payload. Permission set object payload 5 | * aligns to the format used by the solution interfaces Validates if optional 6 | * attributes are present first before triggering the create operation. The code 7 | * reads parameters that are not required by the lambda handler so that the 8 | * subsequent steps in the state machine only get the required data. Alternative 9 | * is for the lambda invocation to send back the full response payload which is 10 | * not required 11 | */ 12 | 13 | /** Get environment variables */ 14 | const { AWS_REGION } = process.env; 15 | 16 | /** SDK and third party client imports */ 17 | import { 18 | AttachManagedPolicyToPermissionSetCommand, 19 | CreatePermissionSetCommand, 20 | PutInlinePolicyToPermissionSetCommand, 21 | SSOAdminClient, 22 | TagResourceCommand, 23 | } from "@aws-sdk/client-sso-admin"; 24 | import { unmarshall } from "@aws-sdk/util-dynamodb"; 25 | import { v4 as uuidv4 } from "uuid"; 26 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 27 | import { serializeDurationToISOFormat } from "../../helpers/src/isoDurationUtility"; 28 | import { logger } from "../../helpers/src/utilities"; 29 | 30 | /** 31 | * SDK and third party client instantiations done as part of init context for 32 | * optimisation purposes. All AWS JS SDK clients are configured with a default 33 | * exponential retry limit of 2 34 | */ 35 | const ssoAdminClientObject = new SSOAdminClient({ 36 | region: AWS_REGION, 37 | maxAttempts: 2, 38 | }); 39 | 40 | /** 41 | * Explicit any as the event is triggered through the state machine with a 42 | * custom payload 43 | */ 44 | /* eslint-disable @typescript-eslint/no-explicit-any */ 45 | export const handler = async (event: any) => { 46 | /** Instantiate UUID based request ID for observability */ 47 | const requestId = uuidv4().toString(); 48 | /** Assign common data elements */ 49 | const instanceArn = event.InstanceArn; 50 | const identityStoreId = event.IdentityStoreId; 51 | const globalAccountAssignmentsTable = event.globalAccountAssignmentsTable; 52 | const pageSize = event.pageSize; 53 | const waitSeconds = event.waitSeconds; 54 | try { 55 | /** Entry log to indicate start of processing */ 56 | logger({ 57 | requestId: requestId, 58 | handler: "rs-create-permission-set-handler", 59 | logMode: logModes.Info, 60 | status: requestStatus.InProgress, 61 | relatedData: event.PermissionSetObject.permissionSetName.S, 62 | }); 63 | /** 64 | * Dynamo DB JSON data needs to be unmarhsalled , otherwise the data 65 | * elements would have keys specific to dynamo DB data types 66 | */ 67 | const permissionSetObject = unmarshall(event.PermissionSetObject); 68 | /** 69 | * Create Permission set with the default attributes. Appending with an 70 | * empty string is to both allow TS type check to pass as well as handle 71 | * scenarios where this could be an empty value 72 | */ 73 | const createOp = await ssoAdminClientObject.send( 74 | new CreatePermissionSetCommand({ 75 | InstanceArn: instanceArn, 76 | Name: permissionSetObject.permissionSetName, 77 | Description: permissionSetObject.permissionSetName, 78 | RelayState: permissionSetObject.relayState + "", 79 | SessionDuration: serializeDurationToISOFormat({ 80 | minutes: parseInt(permissionSetObject.sessionDurationInMinutes + ""), 81 | }), 82 | }), 83 | ); 84 | /** 85 | * Fetch permission set ARN as this is generated run time by the service and 86 | * is referenced in subsequent operations both within the lambda as well as 87 | * for account assignment operations 88 | */ 89 | const permissionSetArn = 90 | createOp.PermissionSet?.PermissionSetArn?.toString() + ""; 91 | /** 92 | * Start processing optional attributes, and therefore check for existence 93 | * of the attributes before provisioning them 94 | */ 95 | if (permissionSetObject.tags.lenth !== 0) { 96 | await ssoAdminClientObject.send( 97 | new TagResourceCommand({ 98 | InstanceArn: instanceArn, 99 | ResourceArn: permissionSetArn, 100 | Tags: permissionSetObject.tags, 101 | }), 102 | ); 103 | } 104 | if (permissionSetObject.managedPoliciesArnList.length !== 0) { 105 | /** 106 | * Based on the recommmendation from service team, addition of managed 107 | * policies to the permission set is serialised instead of being run in 108 | * parallel 109 | */ 110 | for (const managedPolicyArn of permissionSetObject.managedPoliciesArnList) { 111 | await ssoAdminClientObject.send( 112 | new AttachManagedPolicyToPermissionSetCommand({ 113 | InstanceArn: instanceArn, 114 | PermissionSetArn: permissionSetArn, 115 | ManagedPolicyArn: managedPolicyArn, 116 | }), 117 | ); 118 | } 119 | } 120 | if ("inlinePolicyDocument" in permissionSetObject) { 121 | if (Object.keys(permissionSetObject.inlinePolicyDocument).length !== 0) { 122 | await ssoAdminClientObject.send( 123 | new PutInlinePolicyToPermissionSetCommand({ 124 | InstanceArn: instanceArn, 125 | InlinePolicy: JSON.stringify( 126 | permissionSetObject.inlinePolicyDocument, 127 | ), 128 | PermissionSetArn: permissionSetArn, 129 | }), 130 | ); 131 | } 132 | } 133 | /** 134 | * Return the payload back to state machine with the permission set arn for 135 | * next steps i.e. account assignment provisioning 136 | */ 137 | return { 138 | permissionSetName: permissionSetObject.permissionSetName, 139 | permissionSetArn: permissionSetArn, 140 | identityStoreId: identityStoreId, 141 | instanceArn: instanceArn, 142 | globalAccountAssignmentsTable: globalAccountAssignmentsTable, 143 | pageSize: pageSize, 144 | waitSeconds: waitSeconds, 145 | }; 146 | } catch (err) { 147 | /** Generic catch all exception logger */ 148 | logger({ 149 | requestId: requestId, 150 | handler: "rs-create-permission-set-handler", 151 | logMode: logModes.Exception, 152 | status: requestStatus.FailedWithException, 153 | statusMessage: `Permission set create operation failed with exception: ${JSON.stringify( 154 | err, 155 | )} for eventDetail: ${JSON.stringify(event)}`, 156 | }); 157 | /** 158 | * Return payload with exception details in case of failure so that the 159 | * state machine can handle this gracefully 160 | */ 161 | return { 162 | lambdaException: JSON.stringify(err), 163 | }; 164 | } 165 | }; 166 | -------------------------------------------------------------------------------- /lib/lambda-functions/region-switch/src/rs-import-account-assignments.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports account assignments into global tables. Triggered by account 3 | * assignment import SNS topic. Code formats the account assignment data in a 4 | * way that aligns with the extensions solution for re-usability purposes. 5 | */ 6 | 7 | /** Get environment variables */ 8 | const { globalAccountAssignmentsTableName, AWS_REGION } = process.env; 9 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 10 | import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; 11 | /** SDK and third party client imports */ 12 | import { SNSEvent } from "aws-lambda"; 13 | import { v4 as uuidv4 } from "uuid"; 14 | import { 15 | LinkData, 16 | logModes, 17 | requestStatus, 18 | } from "../../helpers/src/interfaces"; 19 | import { logger } from "../../helpers/src/utilities"; 20 | 21 | /** 22 | * SDK and third party client instantiations done as part of init context for 23 | * optimisation purposes. All AWS JS SDK clients are configured with a default 24 | * exponential retry limit of 2 25 | */ 26 | const ddbClientObject = new DynamoDBClient({ 27 | region: AWS_REGION, 28 | maxAttempts: 2, 29 | }); 30 | const ddbDocClientObject = DynamoDBDocumentClient.from(ddbClientObject); 31 | 32 | export const handler = async (event: SNSEvent) => { 33 | /** Instantiate UUID based request ID for observability */ 34 | const requestId = uuidv4().toString(); 35 | try { 36 | const message = JSON.parse(event.Records[0].Sns.Message); 37 | /** 38 | * Format the account assignment data in a format compatible with 39 | * sso-extensions solution 40 | */ 41 | const linkParams: LinkData = { 42 | awsEntityId: `account%${message.linkPayload.awsEntityData}%${message.linkPayload.permissionSetName}%${message.entityName}%${message.entityType}%ssofile`, 43 | awsEntityType: "account", 44 | awsEntityData: message.linkPayload.awsEntityData, 45 | permissionSetName: message.linkPayload.permissionSetName, 46 | principalName: message.entityName, 47 | principalType: message.entityType, 48 | }; 49 | logger({ 50 | handler: "rs-accountAssignmentImporter", 51 | logMode: logModes.Info, 52 | requestId: requestId, 53 | relatedData: linkParams.awsEntityId, 54 | status: requestStatus.InProgress, 55 | sourceRequestId: message.requestId, 56 | statusMessage: `Region switch account assignment import operation in progress`, 57 | }); 58 | /** Upsert account assignment data into the global table */ 59 | await ddbDocClientObject.send( 60 | new PutCommand({ 61 | TableName: globalAccountAssignmentsTableName, 62 | Item: { 63 | ...linkParams, 64 | }, 65 | }), 66 | ); 67 | logger({ 68 | handler: "rs-accountAssignmentImporter", 69 | logMode: logModes.Info, 70 | requestId: requestId, 71 | relatedData: linkParams.awsEntityId, 72 | status: requestStatus.Completed, 73 | sourceRequestId: message.requestId, 74 | statusMessage: `Region switch account assignment import operation in progress`, 75 | }); 76 | } catch (err) { 77 | logger({ 78 | handler: "rs-accountAssignmentImporter", 79 | logMode: logModes.Exception, 80 | status: requestStatus.FailedWithException, 81 | statusMessage: `Account assignment import operation failed with exception: ${JSON.stringify( 82 | err, 83 | )} for eventDetail: ${JSON.stringify(event)}`, 84 | }); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /lib/lambda-functions/region-switch/src/rs-import-permission-sets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports permission sets into global tables. Triggered by permission set 3 | * import topic, formats the permission set object in a format compatible with 4 | * sso-extensions solution, handles optional attributes and upserts the 5 | * permission set object into global tables 6 | */ 7 | 8 | /** Get environment variables */ 9 | const { globalPermissionSetTableName, AWS_REGION } = process.env; 10 | 11 | /** SDK and third party client imports */ 12 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 13 | import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; 14 | import { SNSEvent } from "aws-lambda"; 15 | import { v4 as uuidv4 } from "uuid"; 16 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 17 | import { getMinutesFromISODurationString } from "../../helpers/src/isoDurationUtility"; 18 | import { logger } from "../../helpers/src/utilities"; 19 | 20 | /** 21 | * SDK and third party client instantiations done as part of init context for 22 | * optimisation purposes. All AWS JS SDK clients are configured with a default 23 | * exponential retry limit of 2 24 | */ 25 | const ddbClientObject = new DynamoDBClient({ 26 | region: AWS_REGION, 27 | maxAttempts: 2, 28 | }); 29 | const ddbDocClientObject = DynamoDBDocumentClient.from(ddbClientObject); 30 | 31 | export const handler = async (event: SNSEvent) => { 32 | /** Instantiate UUID based request ID for observability */ 33 | const requestId = uuidv4().toString(); 34 | try { 35 | const message = JSON.parse(event.Records[0].Sns.Message); 36 | const permissionSetName = message.describePermissionSet.PermissionSet.Name; 37 | logger({ 38 | handler: "rs-permissionSetImporter", 39 | logMode: logModes.Info, 40 | requestId: requestId, 41 | relatedData: permissionSetName, 42 | status: requestStatus.InProgress, 43 | sourceRequestId: message.requestId, 44 | statusMessage: `Region switch Permission set import operation in progress`, 45 | }); 46 | 47 | /** 48 | * Construct permission set object from the message payload by instantiating 49 | * all optional attributes as empty, and if the message payload has data for 50 | * these optional attributes use that value, otherwise use the empty 51 | * initialisation 52 | */ 53 | const permissionSetObject = {}; 54 | let computedRelayState = ""; 55 | let computedInlinePolicy = {}; 56 | let computedSessionDurationInMinutes = ""; 57 | const computedManagedPoliciesArnList: Array = []; 58 | if ( 59 | Object.prototype.hasOwnProperty.call( 60 | message.describePermissionSet.PermissionSet, 61 | "RelayState", 62 | ) 63 | ) { 64 | computedRelayState = 65 | message.describePermissionSet.PermissionSet.RelayState; 66 | } 67 | if ( 68 | Object.prototype.hasOwnProperty.call( 69 | message.describePermissionSet.PermissionSet, 70 | "SessionDuration", 71 | ) 72 | ) { 73 | computedSessionDurationInMinutes = getMinutesFromISODurationString( 74 | message.describePermissionSet.PermissionSet.SessionDuration, 75 | ); 76 | } 77 | if ( 78 | message.listManagedPoliciesInPermissionSet.AttachedManagedPolicies 79 | .length > 0 80 | ) { 81 | await Promise.all( 82 | message.listManagedPoliciesInPermissionSet.AttachedManagedPolicies.map( 83 | async (managedPolicy: Record) => { 84 | computedManagedPoliciesArnList.push(managedPolicy.Arn); 85 | }, 86 | ), 87 | ); 88 | } 89 | if (message.getInlinePolicyForPermissionSet.InlinePolicy.length > 0) { 90 | computedInlinePolicy = JSON.parse( 91 | message.getInlinePolicyForPermissionSet.InlinePolicy, 92 | ); 93 | } 94 | /** 95 | * Once all the optional attributes are verified, construct the permission 96 | * set object in a format compatible with sso-extensions and ready for 97 | * upsert 98 | */ 99 | 100 | Object.assign(permissionSetObject, { 101 | permissionSetName: permissionSetName, 102 | sessionDurationInMinutes: computedSessionDurationInMinutes, 103 | relayState: computedRelayState, 104 | tags: message.listTagsForResource.Tags, 105 | managedPoliciesArnList: [...computedManagedPoliciesArnList].sort(), 106 | inlinePolicyDocument: computedInlinePolicy, 107 | }); 108 | /** Upsert the permission set object in the global table */ 109 | await ddbDocClientObject.send( 110 | new PutCommand({ 111 | TableName: globalPermissionSetTableName, 112 | Item: { 113 | ...permissionSetObject, 114 | }, 115 | }), 116 | ); 117 | 118 | logger({ 119 | handler: "rs-permissionSetImporter", 120 | logMode: logModes.Info, 121 | requestId: requestId, 122 | relatedData: permissionSetName, 123 | status: requestStatus.Completed, 124 | sourceRequestId: message.requestId, 125 | statusMessage: `Permission set import completed`, 126 | }); 127 | } catch (err) { 128 | logger({ 129 | handler: "permissionSetImporter", 130 | logMode: logModes.Exception, 131 | status: requestStatus.FailedWithException, 132 | statusMessage: `Permission set import operation failed with exception: ${JSON.stringify( 133 | err, 134 | )} for eventDetail: ${JSON.stringify(event)}`, 135 | }); 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /lib/lambda-functions/region-switch/src/trigger-deploySM.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements custom resource onEvent handler through CDK framework. This custom 3 | * resource maps to triggering the step function, by reading the resource 4 | * properties sent by cloudformation service, and then post triggering the state 5 | * machine, returns back the physical resource ID and state machine execution 6 | * ARN 7 | */ 8 | 9 | /** Get environment variables */ 10 | const { AWS_REGION } = process.env; 11 | 12 | /** SDK and third party client imports */ 13 | import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn"; 14 | import { CloudFormationCustomResourceEvent } from "aws-lambda"; 15 | import { v4 as uuidv4 } from "uuid"; 16 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 17 | import { logger } from "../../helpers/src/utilities"; 18 | /** 19 | * SDK and third party client instantiations done as part of init context for 20 | * optimisation purposes. All AWS JS SDK clients are configured with a default 21 | * exponential retry limit of 2 22 | */ 23 | const sfnClientObject = new SFNClient({ 24 | region: AWS_REGION, 25 | maxAttempts: 2, 26 | }); 27 | 28 | export const handler = async (event: CloudFormationCustomResourceEvent) => { 29 | /** Read the physical resource ID i.e. step function ARN */ 30 | const { deploySMArn } = event.ResourceProperties; 31 | /** Instantiate UUID based request ID for observability */ 32 | const requestId = uuidv4().toString(); 33 | try { 34 | /** 35 | * Get other even resource properties that would be passed to state machine 36 | * for execution 37 | */ 38 | const { 39 | globalPermissionSetsTableName, 40 | globalAccountAssignmentsTable, 41 | CreatePSFunctionName, 42 | } = event.ResourceProperties; 43 | 44 | /** Execute the state machine */ 45 | const stateMachineExecution = await sfnClientObject.send( 46 | new StartExecutionCommand({ 47 | stateMachineArn: deploySMArn, 48 | input: JSON.stringify({ 49 | globalPermissionSetsTableName: globalPermissionSetsTableName, 50 | globalAccountAssignmentsTable: globalAccountAssignmentsTable, 51 | CreatePSFunctionName: CreatePSFunctionName, 52 | waitSeconds: 2, 53 | pageSize: 5, 54 | eventType: event.RequestType, 55 | }), 56 | }), 57 | ); 58 | logger({ 59 | handler: "deploySM", 60 | logMode: logModes.Info, 61 | relatedData: `${stateMachineExecution.executionArn}`, 62 | requestId: requestId, 63 | status: requestStatus.InProgress, 64 | statusMessage: `Custom resource creation triggered stateMachine`, 65 | }); 66 | /** 67 | * Return the step function execution arn, note that we don't return the 68 | * status here as that would be resolved by the isComplete handler 69 | */ 70 | return { 71 | PhysicalResourceId: deploySMArn, 72 | stateMachineExecutionArn: stateMachineExecution.executionArn, 73 | requestId: requestId, 74 | }; 75 | } catch (e) { 76 | logger({ 77 | handler: "deploySM", 78 | logMode: logModes.Exception, 79 | requestId: requestId, 80 | relatedData: `${deploySMArn}`, 81 | status: requestStatus.FailedWithException, 82 | statusMessage: `Custom resource creation failed with exception: ${JSON.stringify( 83 | e, 84 | )}`, 85 | }); 86 | return { 87 | Status: "FAILED", 88 | PhysicalResourceId: deploySMArn, 89 | Reason: `main handler exception in invokeParentSM: ${e}`, 90 | }; 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /lib/lambda-functions/region-switch/src/trigger-parentSM.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements custom resource onEvent handler through CDK framework. This custom 3 | * resource maps to triggering the step function, by reading the resource 4 | * properties sent by cloudformation service, and then post triggering the state 5 | * machine, returns back the physical resource ID and state machine execution 6 | * ARN 7 | */ 8 | 9 | /** Get environment variables */ 10 | 11 | const { AWS_REGION } = process.env; 12 | /** SDK and third party client imports */ 13 | import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn"; 14 | import { CloudFormationCustomResourceEvent } from "aws-lambda"; 15 | import { v4 as uuidv4 } from "uuid"; 16 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 17 | import { logger } from "../../helpers/src/utilities"; 18 | /** 19 | * SDK and third party client instantiations done as part of init context for 20 | * optimisation purposes. All AWS JS SDK clients are configured with a default 21 | * exponential retry limit of 2 22 | */ 23 | const sfnClientObject = new SFNClient({ 24 | region: AWS_REGION, 25 | maxAttempts: 2, 26 | }); 27 | 28 | export const handler = async (event: CloudFormationCustomResourceEvent) => { 29 | /** Read the physical resource ID i.e. step function ARN */ 30 | const { importCurrentConfigSMArn } = event.ResourceProperties; 31 | /** Instantiate UUID based request ID for observability */ 32 | const requestId = uuidv4().toString(); 33 | try { 34 | /** 35 | * Get other even resource properties that would be passed to state machine 36 | * for execution 37 | */ 38 | const { 39 | importAccountAssignmentSMArn, 40 | importPermissionSetSMArn, 41 | accountAssignmentImportTopicArn, 42 | permissionSetImportTopicArn, 43 | temporaryPermissionSetTableName, 44 | } = event.ResourceProperties; 45 | /** Execute the state machine */ 46 | const stateMachineExecution = await sfnClientObject.send( 47 | new StartExecutionCommand({ 48 | stateMachineArn: importCurrentConfigSMArn, 49 | input: JSON.stringify({ 50 | temporaryPermissionSetTableName: temporaryPermissionSetTableName, 51 | importAccountAssignmentSMArn: importAccountAssignmentSMArn, 52 | accountAssignmentImportTopicArn: accountAssignmentImportTopicArn, 53 | importPermissionSetSMArn: importPermissionSetSMArn, 54 | permissionSetImportTopicArn: permissionSetImportTopicArn, 55 | PhysicalResourceId: importCurrentConfigSMArn, 56 | eventType: event.RequestType, 57 | triggerSource: "CloudFormation", 58 | requestId: requestId, 59 | waitSeconds: 2, 60 | pageSize: 5, 61 | }), 62 | }), 63 | ); 64 | logger({ 65 | handler: "parentInvokeSM", 66 | logMode: logModes.Info, 67 | relatedData: `${stateMachineExecution.executionArn}`, 68 | requestId: requestId, 69 | status: requestStatus.InProgress, 70 | statusMessage: `Custom resource creation triggered stateMachine`, 71 | }); 72 | /** 73 | * Return the step function execution arn, note that we don't return the 74 | * status here as that would be resolved by the isComplete handler 75 | */ 76 | return { 77 | PhysicalResourceId: importCurrentConfigSMArn, 78 | stateMachineExecutionArn: stateMachineExecution.executionArn, 79 | requestId: requestId, 80 | }; 81 | } catch (e) { 82 | logger({ 83 | handler: "parentInvokeSM", 84 | logMode: logModes.Exception, 85 | requestId: requestId, 86 | relatedData: `${importCurrentConfigSMArn}`, 87 | status: requestStatus.FailedWithException, 88 | statusMessage: `Custom resource creation failed with exception: ${JSON.stringify( 89 | e, 90 | )}`, 91 | }); 92 | return { 93 | Status: "FAILED", 94 | PhysicalResourceId: importCurrentConfigSMArn, 95 | Reason: `main handler exception in invokeParentSM: ${e}`, 96 | }; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /lib/lambda-functions/region-switch/src/update-custom-resource.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * IsComplete handler implementation for cloud formation custom resource. This 3 | * implementation gets a step function execution arn and is invoked multiple 4 | * times by CDK's custom resource framework handler where the status of the step 5 | * function and isComplete signal is sent back 6 | */ 7 | /** Get environment variables */ 8 | const { ssoRegion, ssoAccountId } = process.env; 9 | 10 | /** SDK and third party client imports */ 11 | import { DescribeExecutionCommand, SFNClient } from "@aws-sdk/client-sfn"; 12 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 13 | import { logger, StateMachineError } from "../../helpers/src/utilities"; 14 | /** 15 | * SDK and third party client instantiations done as part of init context for 16 | * optimisation purposes. All AWS JS SDK clients are configured with a default 17 | * exponential retry limit of 2 18 | */ 19 | const sfnClientObject = new SFNClient({ 20 | region: ssoRegion, 21 | maxAttempts: 2, 22 | }); 23 | 24 | /* eslint-disable @typescript-eslint/no-explicit-any */ 25 | export const handler = async (event: any) => { 26 | /** Get request ID and state machine execution arn from CloudFormation */ 27 | const { stateMachineExecutionArn, requestId } = event; 28 | try { 29 | logger({ 30 | handler: "updateCustomResource", 31 | logMode: logModes.Info, 32 | relatedData: `${stateMachineExecutionArn}`, 33 | requestId: requestId, 34 | status: requestStatus.InProgress, 35 | statusMessage: `Custom resource update - stateMachine status inquiry start`, 36 | }); 37 | /** Fetch the current status of the state machine execution */ 38 | const stateMachineExecutionResult = await sfnClientObject.send( 39 | new DescribeExecutionCommand({ 40 | executionArn: stateMachineExecutionArn, 41 | }), 42 | ); 43 | /** 44 | * Handle the update back to the custom resource framework based on the 45 | * current state machine execution status 46 | */ 47 | 48 | switch (stateMachineExecutionResult.status) { 49 | case "RUNNING": { 50 | logger({ 51 | handler: "updateCustomResource", 52 | logMode: logModes.Info, 53 | requestId: requestId, 54 | relatedData: `${stateMachineExecutionArn}`, 55 | status: requestStatus.InProgress, 56 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} still running`, 57 | }); 58 | return { 59 | IsComplete: false, 60 | }; 61 | } 62 | case "SUCCEEDED": { 63 | logger({ 64 | handler: "updateCustomResource", 65 | logMode: logModes.Info, 66 | requestId: requestId, 67 | relatedData: `${stateMachineExecutionArn}`, 68 | status: requestStatus.Completed, 69 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} complete`, 70 | }); 71 | return { 72 | IsComplete: true, 73 | }; 74 | } 75 | case "FAILED": { 76 | logger({ 77 | handler: "updateCustomResource", 78 | logMode: logModes.Exception, 79 | requestId: requestId, 80 | relatedData: `${stateMachineExecutionArn}`, 81 | status: requestStatus.FailedWithError, 82 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with error. See error details in ${ssoAccountId} account, ${ssoRegion} region`, 83 | }); 84 | throw new StateMachineError({ 85 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with error. See error details in ${ssoAccountId} account, ${ssoRegion} region`, 86 | }); 87 | } 88 | case "TIMED_OUT": { 89 | logger({ 90 | handler: "updateCustomResource", 91 | logMode: logModes.Exception, 92 | requestId: requestId, 93 | relatedData: `${stateMachineExecutionArn}`, 94 | status: requestStatus.FailedWithError, 95 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} timed out. See timeout details in ${ssoAccountId} account, ${ssoRegion} region`, 96 | }); 97 | throw new StateMachineError({ 98 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} timed out. See timeout details in ${ssoAccountId} account, ${ssoRegion} region`, 99 | }); 100 | } 101 | default: { 102 | logger({ 103 | handler: "updateCustomResource", 104 | logMode: logModes.Exception, 105 | relatedData: `${stateMachineExecutionArn}`, 106 | status: requestStatus.FailedWithError, 107 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} reached an unknown status. See details in ${ssoAccountId} account, ${ssoRegion} region`, 108 | }); 109 | throw new StateMachineError({ 110 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} reached an unknown status. See details in ${ssoAccountId} account, ${ssoRegion} region`, 111 | }); 112 | } 113 | } 114 | } catch (e) { 115 | if (e instanceof StateMachineError) { 116 | throw e; 117 | } else { 118 | logger({ 119 | handler: "updateCustomResource", 120 | logMode: logModes.Exception, 121 | requestId: requestId, 122 | relatedData: `${stateMachineExecutionArn}`, 123 | status: requestStatus.FailedWithException, 124 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with exception: ${JSON.stringify( 125 | e, 126 | )}`, 127 | }); 128 | throw new Error( 129 | `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with exception: ${JSON.stringify( 130 | e, 131 | )}`, 132 | ); 133 | } 134 | } 135 | }; 136 | -------------------------------------------------------------------------------- /lib/lambda-functions/upgrade-to-v303/src/processLinkData.ts: -------------------------------------------------------------------------------- 1 | export interface LinkDataToProcess { 2 | readonly oldlinkData: string; 3 | readonly artefactsBucketName: string; 4 | readonly linksTableName: string; 5 | } 6 | 7 | export const handler = async (event: LinkDataToProcess) => { 8 | const { oldlinkData, artefactsBucketName, linksTableName } = event; 9 | const processedOldLinkData = decodeURIComponent(oldlinkData.split("/")[1]); 10 | if (processedOldLinkData.endsWith(".ssofile")) { 11 | const keyValue = processedOldLinkData.split("."); 12 | return { 13 | oldlinkData: processedOldLinkData, 14 | awsEntityId: `${keyValue[0]}%${keyValue[1]}%${keyValue[2]}%${keyValue 15 | .slice(3, -2) 16 | .join(".")}%${keyValue[4]}%ssofile`, 17 | awsEntityType: keyValue[0], 18 | awsEntityData: keyValue[1], 19 | permissionSetName: keyValue[2], 20 | principalName: keyValue.slice(3, -2).join("."), 21 | principalType: keyValue[4], 22 | artefactsBucketName: artefactsBucketName, 23 | linksTableName: linksTableName, 24 | process: true, 25 | }; 26 | } else { 27 | return { 28 | process: false, 29 | oldlinkData: processedOldLinkData, 30 | awsEntityId: "", 31 | awsEntityType: "", 32 | awsEntityData: "", 33 | permissionSetName: "", 34 | principalName: "", 35 | principalType: "", 36 | artefactsBucketName: artefactsBucketName, 37 | linksTableName: linksTableName, 38 | }; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /lib/lambda-functions/upgrade-to-v303/src/triggerV303SM.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Objective: Implement custom resource that invokes upgrade V303 state machine 3 | * in target account Trigger source: Cloudformation custom resource provider 4 | * framework 5 | * 6 | * - Invoke the state machine witht the payload 7 | * - If the request type is delete, we don't do anything as this is a invoke type 8 | * custom resource 9 | */ 10 | const { AWS_REGION } = process.env; 11 | 12 | // Lambda types import 13 | // SDK and third party client imports 14 | import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn"; 15 | import { CloudFormationCustomResourceEvent } from "aws-lambda"; 16 | import { v4 as uuidv4 } from "uuid"; 17 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 18 | import { logger } from "../../helpers/src/utilities"; 19 | 20 | const sfnClientObject = new SFNClient({ 21 | region: AWS_REGION, 22 | maxAttempts: 2, 23 | }); 24 | 25 | export const handler = async (event: CloudFormationCustomResourceEvent) => { 26 | const { upgradeV303SMArn } = event.ResourceProperties; 27 | const requestId = uuidv4().toString(); 28 | try { 29 | const { artefactsBucketName, linksTableName, processLinksFunctionName } = 30 | event.ResourceProperties; 31 | 32 | const stateMachineExecution = await sfnClientObject.send( 33 | new StartExecutionCommand({ 34 | stateMachineArn: upgradeV303SMArn, 35 | input: JSON.stringify({ 36 | artefactsBucketName: artefactsBucketName, 37 | linksTableName: linksTableName, 38 | processLinksFunctionName: processLinksFunctionName, 39 | eventType: event.RequestType, 40 | }), 41 | }), 42 | ); 43 | logger({ 44 | handler: "upgradeSM", 45 | logMode: logModes.Info, 46 | relatedData: `${stateMachineExecution.executionArn}`, 47 | requestId: requestId, 48 | status: requestStatus.InProgress, 49 | statusMessage: `Custom resource creation triggered stateMachine`, 50 | }); 51 | //No status return as it would be updated by the state machine 52 | return { 53 | PhysicalResourceId: upgradeV303SMArn, 54 | stateMachineExecutionArn: stateMachineExecution.executionArn, 55 | requestId: requestId, 56 | }; 57 | } catch (e) { 58 | logger({ 59 | handler: "upgradeSM", 60 | logMode: logModes.Exception, 61 | requestId: requestId, 62 | relatedData: `${upgradeV303SMArn}`, 63 | status: requestStatus.FailedWithException, 64 | statusMessage: `Custom resource creation failed with exception: ${JSON.stringify( 65 | e, 66 | )}`, 67 | }); 68 | return { 69 | Status: "FAILED", 70 | PhysicalResourceId: upgradeV303SMArn, 71 | Reason: `main handler exception in invokeParentSM: ${e}`, 72 | }; 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /lib/lambda-functions/upgrade-to-v303/src/update-custom-resource.ts: -------------------------------------------------------------------------------- 1 | /** Objective: Update cloudformation with custom resource status */ 2 | const { AWS_REGION } = process.env; 3 | 4 | // Lambda types import 5 | // SDK and third party client imports 6 | import { DescribeExecutionCommand, SFNClient } from "@aws-sdk/client-sfn"; 7 | import { logModes, requestStatus } from "../../helpers/src/interfaces"; 8 | import { logger, StateMachineError } from "../../helpers/src/utilities"; 9 | 10 | const sfnClientObject = new SFNClient({ 11 | region: AWS_REGION, 12 | maxAttempts: 2, 13 | }); 14 | 15 | /* eslint-disable @typescript-eslint/no-explicit-any */ 16 | export const handler = async (event: any) => { 17 | //event is of any type and not CloudFormationCustomResource as it does not allow state to be passed between onEvent and isComplete handlers 18 | const { stateMachineExecutionArn, requestId } = event; 19 | try { 20 | logger({ 21 | handler: "updateCustomResource", 22 | logMode: logModes.Info, 23 | relatedData: `${stateMachineExecutionArn}`, 24 | requestId: requestId, 25 | status: requestStatus.InProgress, 26 | statusMessage: `Custom resource update - stateMachine status inquiry start`, 27 | }); 28 | const stateMachineExecutionResult = await sfnClientObject.send( 29 | new DescribeExecutionCommand({ 30 | executionArn: stateMachineExecutionArn, 31 | }), 32 | ); 33 | 34 | switch (stateMachineExecutionResult.status) { 35 | case "RUNNING": { 36 | logger({ 37 | handler: "updateCustomResource", 38 | logMode: logModes.Info, 39 | requestId: requestId, 40 | relatedData: `${stateMachineExecutionArn}`, 41 | status: requestStatus.InProgress, 42 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} still running`, 43 | }); 44 | return { 45 | IsComplete: false, 46 | }; 47 | } 48 | case "SUCCEEDED": { 49 | logger({ 50 | handler: "updateCustomResource", 51 | logMode: logModes.Info, 52 | requestId: requestId, 53 | relatedData: `${stateMachineExecutionArn}`, 54 | status: requestStatus.Completed, 55 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} complete`, 56 | }); 57 | return { 58 | IsComplete: true, 59 | }; 60 | } 61 | case "FAILED": { 62 | logger({ 63 | handler: "updateCustomResource", 64 | logMode: logModes.Exception, 65 | requestId: requestId, 66 | relatedData: `${stateMachineExecutionArn}`, 67 | status: requestStatus.FailedWithError, 68 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with error.`, 69 | }); 70 | throw new StateMachineError({ 71 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with error.`, 72 | }); 73 | } 74 | case "TIMED_OUT": { 75 | logger({ 76 | handler: "updateCustomResource", 77 | logMode: logModes.Exception, 78 | requestId: requestId, 79 | relatedData: `${stateMachineExecutionArn}`, 80 | status: requestStatus.FailedWithError, 81 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} timed out.`, 82 | }); 83 | throw new StateMachineError({ 84 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} timed out.`, 85 | }); 86 | } 87 | default: { 88 | logger({ 89 | handler: "updateCustomResource", 90 | logMode: logModes.Exception, 91 | relatedData: `${stateMachineExecutionArn}`, 92 | status: requestStatus.FailedWithError, 93 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} reached an unknown status.`, 94 | }); 95 | throw new StateMachineError({ 96 | message: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} reached an unknown status.`, 97 | }); 98 | } 99 | } 100 | } catch (e) { 101 | if (e instanceof StateMachineError) { 102 | throw e; 103 | } else { 104 | logger({ 105 | handler: "updateCustomResource", 106 | logMode: logModes.Exception, 107 | requestId: requestId, 108 | relatedData: `${stateMachineExecutionArn}`, 109 | status: requestStatus.FailedWithException, 110 | statusMessage: `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with exception: ${JSON.stringify( 111 | e, 112 | )}`, 113 | }); 114 | throw new Error( 115 | `Custom resource update - stateMachine with execution arn: ${stateMachineExecutionArn} failed with exception: ${JSON.stringify( 116 | e, 117 | )}`, 118 | ); 119 | } 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /lib/lambda-layers/nodejs-layer/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-sso-extensions-for-enterprise-layer", 3 | "version": "3.1.9", 4 | "description": "AWS SSO Permissions Utility Layer", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "dependencies": { 10 | "@aws-sdk/client-dynamodb": "^3.172.0", 11 | "@aws-sdk/client-identitystore": "^3.171.0", 12 | "@aws-sdk/client-organizations": "^3.171.0", 13 | "@aws-sdk/client-s3": "^3.171.0", 14 | "@aws-sdk/client-sfn": "^3.171.0", 15 | "@aws-sdk/client-sns": "^3.171.0", 16 | "@aws-sdk/client-sqs": "^3.171.0", 17 | "@aws-sdk/client-ssm": "^3.171.0", 18 | "@aws-sdk/client-sso-admin": "^3.171.0", 19 | "@aws-sdk/credential-providers": "^3.171.0", 20 | "@aws-sdk/lib-dynamodb": "^3.172.0", 21 | "@aws-sdk/util-dynamodb": "^3.172.0", 22 | "@aws-sdk/util-waiter": "^3.171.0", 23 | "ajv": "^8.11.0", 24 | "json-diff": "^0.9.0", 25 | "uuid": "^9.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/json-diff": "^0.9.0", 29 | "@types/uuid": "^8.3.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/payload-schema-definitions/Link-API.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "Link-API", 4 | "type": "object", 5 | "title": "Link-API", 6 | "description": "Schemea for link create/delete.", 7 | "required": ["action", "linkData"], 8 | "properties": { 9 | "action": { 10 | "$id": "#/properties/action", 11 | "type": "string", 12 | "title": "Link operation type", 13 | "description": "Link operation type", 14 | "enum": ["create", "delete"] 15 | }, 16 | "linkData": { 17 | "$id": "#/properties/linkData", 18 | "type": "string", 19 | "title": "Link ojbect", 20 | "description": "Link object", 21 | "oneOf": [ 22 | { 23 | "pattern": "root%all%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 24 | }, 25 | { 26 | "pattern": "root%all%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 27 | }, 28 | { 29 | "pattern": "ou_id%ou-[a-z0-9]{4}-[a-z0-9]{8}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 30 | }, 31 | { 32 | "pattern": "ou_id%ou-[a-z0-9]{4}-[a-z0-9]{8}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 33 | }, 34 | { 35 | "pattern": "account%\\d{12}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 36 | }, 37 | { 38 | "pattern": "account\\d{12}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 39 | }, 40 | { 41 | "pattern": "account_tag%[\\w+=,.@-]{1,128}\\^[\\w+=,.@-]{1,256}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 42 | }, 43 | { 44 | "pattern": "account_tag%[\\w+=,.@-]{1,128}\\^[\\w+=,.@-]{1,256}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 45 | } 46 | ] 47 | } 48 | }, 49 | "additionalProperties": false 50 | } 51 | -------------------------------------------------------------------------------- /lib/payload-schema-definitions/Link-S3.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "Link-S3", 4 | "type": "object", 5 | "title": "Link-S3", 6 | "description": "Schemea for link create/delete through S3 interface.", 7 | "required": ["linkData"], 8 | "properties": { 9 | "linkData": { 10 | "$id": "#/properties/linkData", 11 | "type": "string", 12 | "title": "Link ojbect", 13 | "description": "Link object", 14 | "oneOf": [ 15 | { 16 | "pattern": "root%all%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 17 | }, 18 | { 19 | "pattern": "root%all%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 20 | }, 21 | { 22 | "pattern": "ou_id%ou-[a-z0-9]{4}-[a-z0-9]{8}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 23 | }, 24 | { 25 | "pattern": "ou_id%ou-[a-z0-9]{4}-[a-z0-9]{8}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 26 | }, 27 | { 28 | "pattern": "account%\\d{12}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 29 | }, 30 | { 31 | "pattern": "account%\\d{12}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 32 | }, 33 | { 34 | "pattern": "account_tag%[\\w+=,.@-]{1,128}\\^[\\w+=,.@-]{1,256}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,1024}%GROUP%ssofile" 35 | }, 36 | { 37 | "pattern": "account_tag%[\\w+=,.@-]{1,128}\\^[\\w+=,.@-]{1,256}%[\\w+=,.@-]{1,32}%[_\\-\\p{L}\\p{M}\\p{S}\\p{N}+@.]{1,128}%USER%ssofile" 38 | } 39 | ] 40 | } 41 | }, 42 | "additionalProperties": false 43 | } 44 | -------------------------------------------------------------------------------- /lib/payload-schema-definitions/PermissionSet-DeleteAPI.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "http://example.com/example.json", 4 | "type": "object", 5 | "title": "PostPayload", 6 | "description": "Schemea for permission set payload.", 7 | "required": ["action", "permissionSetData"], 8 | "properties": { 9 | "action": { 10 | "$id": "#/properties/action", 11 | "type": "string", 12 | "title": "Action $schema", 13 | "description": "Permission set operation type", 14 | "enum": ["delete"] 15 | }, 16 | "permissionSetData": { 17 | "$id": "#/properties/permissionSetData", 18 | "type": "object", 19 | "title": "The permissionSetData $schema", 20 | "description": "Captures the complete permission set", 21 | "required": ["permissionSetName"], 22 | "properties": { 23 | "permissionSetName": { 24 | "$id": "#/properties/permissionSetData/properties/permissionSetName", 25 | "type": "string", 26 | "title": "The permissionSetName $schema", 27 | "description": "Name of the permission set name", 28 | "minLength": 1, 29 | "maxLength": 32, 30 | "pattern": "^[A-Za-z0-9\\+=,.@-]+$" 31 | } 32 | }, 33 | "additionalProperties": false 34 | } 35 | }, 36 | "additionalProperties": false 37 | } 38 | -------------------------------------------------------------------------------- /lib/stacks/pipeline/aws-sso-extensions-for-enterprise.ts: -------------------------------------------------------------------------------- 1 | /** Main pipeline stack i.e. entry point of the application */ 2 | 3 | import { Stack, StackProps, Tags } from "aws-cdk-lib"; 4 | import { Repository } from "aws-cdk-lib/aws-codecommit"; 5 | import { 6 | CodePipeline, 7 | CodePipelineSource, 8 | IFileSetProducer, 9 | ShellStep, 10 | } from "aws-cdk-lib/pipelines"; 11 | import { S3Trigger } from "aws-cdk-lib/aws-codepipeline-actions"; 12 | import { Bucket } from "aws-cdk-lib/aws-s3"; 13 | import { Construct } from "constructs"; 14 | import { BuildConfig } from "../../build/buildConfig"; 15 | import { 16 | OrgArtefactsDeploymentStage, 17 | SolutionArtefactsDeploymentStage, 18 | SSOArtefactsDeploymentStage, 19 | } from "./pipeline-stages"; 20 | 21 | function fullname(buildConfig: BuildConfig, name: string): string { 22 | return buildConfig.Environment + "-" + buildConfig.App + "-" + name; 23 | } 24 | 25 | export class AwsSsoExtensionsForEnterprise extends Stack { 26 | constructor( 27 | scope: Construct, 28 | id: string, 29 | props: StackProps | undefined, 30 | buildConfig: BuildConfig, 31 | ) { 32 | super(scope, id, props); 33 | 34 | /** Instantiate this to empty file set producer initially */ 35 | let inputSource: IFileSetProducer = {}; 36 | /** 37 | * Based on the pipeline source type, instantiate the source connection 38 | * appropriately 39 | */ 40 | if (buildConfig.PipelineSettings.RepoType.toLowerCase() === "codecommit") { 41 | inputSource = CodePipelineSource.codeCommit( 42 | Repository.fromRepositoryArn( 43 | this, 44 | fullname(buildConfig, "importedCodeCommitRepo"), 45 | buildConfig.PipelineSettings.RepoArn, 46 | ), 47 | buildConfig.PipelineSettings.RepoBranchName, 48 | ); 49 | } else if ( 50 | buildConfig.PipelineSettings.RepoType.toLowerCase() === "codestar" 51 | ) { 52 | inputSource = CodePipelineSource.connection( 53 | buildConfig.PipelineSettings.RepoName, 54 | buildConfig.PipelineSettings.RepoBranchName, 55 | { 56 | connectionArn: buildConfig.PipelineSettings.CodeStarConnectionArn, 57 | }, 58 | ); 59 | } else if (buildConfig.PipelineSettings.RepoType.toLowerCase() === "s3") { 60 | const sourceBucketName = 61 | buildConfig.PipelineSettings.SourceBucketName || ""; 62 | const sourceBucketKey = 63 | buildConfig.PipelineSettings.SourceObjectKey || ""; 64 | 65 | const sourceBucket = Bucket.fromBucketName( 66 | this, 67 | fullname(buildConfig, "importedSourceBucket"), 68 | sourceBucketName, 69 | ); 70 | inputSource = CodePipelineSource.s3(sourceBucket, sourceBucketKey, { 71 | trigger: S3Trigger.NONE, 72 | }); 73 | } 74 | const pipeline = new CodePipeline(this, fullname(buildConfig, "pipeline"), { 75 | pipelineName: fullname(buildConfig, "pipeline"), 76 | crossAccountKeys: true, 77 | publishAssetsInParallel: false, 78 | synth: new ShellStep(fullname(buildConfig, "synth"), { 79 | input: inputSource, 80 | commands: [ 81 | "yarn global add aws-cdk@2.x", //Because CodeBuild standard 5.0 does not yet have AWS CDK monorepo as default CDK package 82 | "mkdir ./lib/lambda-layers/nodejs-layer/nodejs/payload-schema-definitions", 83 | "cp -R ./lib/payload-schema-definitions/* ./lib/lambda-layers/nodejs-layer/nodejs/payload-schema-definitions/", 84 | "yarn --cwd ./lib/lambda-layers/nodejs-layer/nodejs install --frozen-lockfile --silent", 85 | "yarn --cwd ./lib/lambda-functions install --frozen-lockfile --silent", 86 | "yarn install --frozen-lockfile --silent", 87 | "yarn build", 88 | buildConfig.PipelineSettings.SynthCommand, 89 | ], 90 | }), 91 | }); 92 | 93 | const deployOrgArtefacts = new OrgArtefactsDeploymentStage( 94 | this, 95 | fullname(buildConfig, "deployOrgArtefacts"), 96 | { 97 | env: { 98 | account: buildConfig.PipelineSettings.OrgMainAccountId, 99 | region: "us-east-1", 100 | }, 101 | }, 102 | buildConfig, 103 | ); 104 | 105 | Tags.of(deployOrgArtefacts).add("App", buildConfig.App); 106 | Tags.of(deployOrgArtefacts).add("Environment", buildConfig.Environment); 107 | 108 | pipeline.addStage(deployOrgArtefacts); 109 | 110 | const deploySSOArtefacts = new SSOArtefactsDeploymentStage( 111 | this, 112 | fullname(buildConfig, "deploySSOArtefacts"), 113 | { 114 | env: { 115 | account: buildConfig.PipelineSettings.SSOServiceAccountId, 116 | region: buildConfig.PipelineSettings.SSOServiceAccountRegion, 117 | }, 118 | }, 119 | buildConfig, 120 | ); 121 | 122 | Tags.of(deploySSOArtefacts).add("App", buildConfig.App); 123 | Tags.of(deploySSOArtefacts).add("Environment", buildConfig.Environment); 124 | 125 | pipeline.addStage(deploySSOArtefacts); 126 | 127 | const deploySolutionArtefacts = new SolutionArtefactsDeploymentStage( 128 | this, 129 | fullname(buildConfig, "deploySolutionArtefacts"), 130 | { 131 | env: { 132 | account: buildConfig.PipelineSettings.TargetAccountId, 133 | region: buildConfig.PipelineSettings.TargetAccountRegion, 134 | }, 135 | }, 136 | buildConfig, 137 | ); 138 | 139 | Tags.of(deploySolutionArtefacts).add("App", buildConfig.App); 140 | Tags.of(deploySolutionArtefacts).add( 141 | "Environment", 142 | buildConfig.Environment, 143 | ); 144 | 145 | pipeline.addStage(deploySolutionArtefacts); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/stacks/pipeline/pipeline-stages.ts: -------------------------------------------------------------------------------- 1 | /** Pipeline stages abstraction */ 2 | 3 | import { DefaultStackSynthesizer, Stage, StageProps } from "aws-cdk-lib"; 4 | import { Construct } from "constructs"; 5 | import { BuildConfig } from "../../build/buildConfig"; 6 | import { ManagedPolicies } from "../pipelineStageStacks/managed-policies"; 7 | import { OrgEventsProcessor } from "../pipelineStageStacks/org-events-processor"; 8 | import { PreSolutionArtefacts } from "../pipelineStageStacks/pre-solution-artefacts"; 9 | import { SolutionArtefacts } from "../pipelineStageStacks/solution-artefacts"; 10 | import { SSOApiRoles } from "../pipelineStageStacks/sso-api-roles"; 11 | import { SSOEventsProcessor } from "../pipelineStageStacks/sso-events-processor"; 12 | import { SSOImportArtefactsPart1 } from "../pipelineStageStacks/sso-import-artefacts-part1"; 13 | import { SSOImportArtefactsPart2 } from "../pipelineStageStacks/sso-import-artefacts-part2"; 14 | import { UpgradeToV303 } from "../pipelineStageStacks/upgrade-to-v303"; 15 | 16 | function fullname(buildConfig: BuildConfig, name: string): string { 17 | return buildConfig.Environment + "-" + buildConfig.App + "-" + name; 18 | } 19 | 20 | export class OrgArtefactsDeploymentStage extends Stage { 21 | constructor( 22 | scope: Construct, 23 | id: string, 24 | props: StageProps | undefined, 25 | buildConfig: BuildConfig, 26 | ) { 27 | super(scope, id, props); 28 | 29 | new OrgEventsProcessor( 30 | this, 31 | fullname(buildConfig, "orgEventsProcessorStack"), 32 | { 33 | stackName: fullname(buildConfig, "orgEventsProcessorStack"), 34 | synthesizer: new DefaultStackSynthesizer({ 35 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 36 | }), 37 | }, 38 | buildConfig, 39 | ); 40 | } 41 | } 42 | 43 | export class SSOArtefactsDeploymentStage extends Stage { 44 | constructor( 45 | scope: Construct, 46 | id: string, 47 | props: StageProps | undefined, 48 | buildConfig: BuildConfig, 49 | ) { 50 | super(scope, id, props); 51 | 52 | new SSOEventsProcessor( 53 | this, 54 | fullname(buildConfig, "ssoEventsProcessorStack"), 55 | { 56 | stackName: fullname(buildConfig, "ssoEventsProcessorStack"), 57 | synthesizer: new DefaultStackSynthesizer({ 58 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 59 | }), 60 | }, 61 | buildConfig, 62 | ); 63 | 64 | new SSOApiRoles( 65 | this, 66 | fullname(buildConfig, "ssoAPIRolesstack"), 67 | { 68 | stackName: fullname(buildConfig, "ssoAPIRolesstack"), 69 | synthesizer: new DefaultStackSynthesizer({ 70 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 71 | }), 72 | }, 73 | buildConfig, 74 | ); 75 | 76 | new ManagedPolicies( 77 | this, 78 | fullname(buildConfig, "managedPoliciesStack"), 79 | { 80 | stackName: fullname(buildConfig, "managedPoliciesStack"), 81 | synthesizer: new DefaultStackSynthesizer({ 82 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 83 | }), 84 | }, 85 | buildConfig, 86 | ); 87 | 88 | if (buildConfig.Parameters.ImportCurrentSSOConfiguration) { 89 | new SSOImportArtefactsPart1( 90 | this, 91 | fullname(buildConfig, "ssoImportArtefactsPart1stack"), 92 | { 93 | stackName: fullname(buildConfig, "ssoImportArtefactsPart1stack"), 94 | synthesizer: new DefaultStackSynthesizer({ 95 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 96 | }), 97 | }, 98 | buildConfig, 99 | ); 100 | } 101 | } 102 | } 103 | 104 | export class SolutionArtefactsDeploymentStage extends Stage { 105 | constructor( 106 | scope: Construct, 107 | id: string, 108 | props: StageProps | undefined, 109 | buildConfig: BuildConfig, 110 | ) { 111 | super(scope, id, props); 112 | 113 | const preSolutionArtefactsStack = new PreSolutionArtefacts( 114 | this, 115 | fullname(buildConfig, "preSolutionArtefactsStack"), 116 | { 117 | stackName: fullname(buildConfig, "preSolutionArtefactsStack"), 118 | synthesizer: new DefaultStackSynthesizer({ 119 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 120 | }), 121 | }, 122 | buildConfig, 123 | ); 124 | 125 | const solutionartefactsStack = new SolutionArtefacts( 126 | this, 127 | fullname(buildConfig, "solutionartefactsStack"), 128 | { 129 | stackName: fullname(buildConfig, "solutionartefactsStack"), 130 | synthesizer: new DefaultStackSynthesizer({ 131 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 132 | }), 133 | }, 134 | buildConfig, 135 | ); 136 | 137 | solutionartefactsStack.node.addDependency(preSolutionArtefactsStack); 138 | 139 | if (buildConfig.Parameters.ImportCurrentSSOConfiguration) { 140 | const ssoImportArtefactsPart2Stack = new SSOImportArtefactsPart2( 141 | this, 142 | fullname(buildConfig, "ssoImportArtefactsPart2Stack"), 143 | { 144 | stackName: fullname(buildConfig, "ssoImportArtefactsPart2Stack"), 145 | synthesizer: new DefaultStackSynthesizer({ 146 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 147 | }), 148 | }, 149 | buildConfig, 150 | ); 151 | 152 | ssoImportArtefactsPart2Stack.node.addDependency(solutionartefactsStack); 153 | } 154 | 155 | if (buildConfig.Parameters.UpgradeFromVersionLessThanV303) { 156 | const upgradeToV303Stack = new UpgradeToV303( 157 | this, 158 | fullname(buildConfig, "upgradeToV303Stack"), 159 | { 160 | stackName: fullname(buildConfig, "upgradeToV303Stack"), 161 | synthesizer: new DefaultStackSynthesizer({ 162 | qualifier: buildConfig.PipelineSettings.BootstrapQualifier, 163 | }), 164 | }, 165 | buildConfig, 166 | ); 167 | upgradeToV303Stack.node.addDependency(solutionartefactsStack); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/stacks/pipelineStageStacks/pre-solution-artefacts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deploys Core solution components The stack itself does not have any direct 3 | * resource deployment and abstracts all resource deployments to the constructs 4 | */ 5 | import { Stack, StackProps } from "aws-cdk-lib"; 6 | import { Construct } from "constructs"; 7 | import { BuildConfig } from "../../build/buildConfig"; 8 | import { IndependentUtility } from "../../constructs/independent-utlity"; 9 | import { LambdaLayers } from "../../constructs/lambda-layers"; 10 | import { LinkCRUD } from "../../constructs/link-crud"; 11 | import { PermissionSetCRUD } from "../../constructs/permission-set-crud"; 12 | import { PreSolutionAccessManager } from "../../constructs/preSolution-access-manager"; 13 | import { Utility } from "../../constructs/utility"; 14 | 15 | function name(buildConfig: BuildConfig, resourcename: string): string { 16 | return buildConfig.Environment + "-" + resourcename; 17 | } 18 | 19 | export class PreSolutionArtefacts extends Stack { 20 | public readonly deployIndependentUtility: IndependentUtility; 21 | public readonly deployLambdaLayers: LambdaLayers; 22 | public readonly deployLinkCRUD: LinkCRUD; 23 | public readonly deployUtility: Utility; 24 | public readonly deployPermissionSetCRUD: PermissionSetCRUD; 25 | constructor( 26 | scope: Construct, 27 | id: string, 28 | props: StackProps | undefined, 29 | buildConfig: BuildConfig, 30 | ) { 31 | super(scope, id, props); 32 | 33 | this.deployLambdaLayers = new LambdaLayers( 34 | this, 35 | name(buildConfig, "lambdaLayers"), 36 | buildConfig, 37 | ); 38 | 39 | this.deployIndependentUtility = new IndependentUtility( 40 | this, 41 | name(buildConfig, "independentUtility"), 42 | buildConfig, 43 | ); 44 | 45 | this.deployLinkCRUD = new LinkCRUD( 46 | this, 47 | name(buildConfig, "linkCRUD"), 48 | buildConfig, 49 | { 50 | nodeJsLayer: this.deployLambdaLayers.nodeJsLayer, 51 | errorNotificationsTopicArn: 52 | this.deployIndependentUtility.errorNotificationsTopic.topicArn, 53 | ssoArtefactsBucket: this.deployIndependentUtility.ssoArtefactsBucket, 54 | ddbTablesKey: this.deployIndependentUtility.ddbTablesKey, 55 | logsKey: this.deployIndependentUtility.logsKey, 56 | snsTopicsKey: this.deployIndependentUtility.snsTopicsKey, 57 | }, 58 | ); 59 | 60 | this.deployUtility = new Utility( 61 | this, 62 | name(buildConfig, "utility"), 63 | buildConfig, 64 | ); 65 | 66 | this.deployPermissionSetCRUD = new PermissionSetCRUD( 67 | this, 68 | name(buildConfig, "permissionSetCRUD"), 69 | buildConfig, 70 | { 71 | nodeJsLayer: this.deployLambdaLayers.nodeJsLayer, 72 | linksTableName: this.deployLinkCRUD.linksTable.tableName, 73 | errorNotificationsTopicArn: 74 | this.deployIndependentUtility.errorNotificationsTopic.topicArn, 75 | ssoArtefactsBucket: this.deployIndependentUtility.ssoArtefactsBucket, 76 | ddbTablesKey: this.deployIndependentUtility.ddbTablesKey, 77 | logsKey: this.deployIndependentUtility.logsKey, 78 | snsTopicsKey: this.deployIndependentUtility.snsTopicsKey, 79 | }, 80 | ); 81 | 82 | new PreSolutionAccessManager( 83 | this, 84 | name(buildConfig, "preSolutionAccessManager"), 85 | buildConfig, 86 | { 87 | IndependentUtility: this.deployIndependentUtility, 88 | LinkCRUD: this.deployLinkCRUD, 89 | PermissionSetCRUD: this.deployPermissionSetCRUD, 90 | Utility: this.deployUtility, 91 | }, 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/stacks/pipelineStageStacks/sso-api-roles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deploys roles in SSO account that allow SSO admin API, Identity Store api 3 | * access from target account 4 | */ 5 | import { PolicyStatement } from "aws-cdk-lib/aws-iam"; 6 | import { Stack, StackProps } from "aws-cdk-lib"; 7 | import { Construct } from "constructs"; 8 | import { BuildConfig } from "../../build/buildConfig"; 9 | import { CrossAccountRole } from "../../constructs/cross-account-role"; 10 | 11 | function name(buildConfig: BuildConfig, resourcename: string): string { 12 | return buildConfig.Environment + "-" + resourcename; 13 | } 14 | 15 | export class SSOApiRoles extends Stack { 16 | constructor( 17 | scope: Construct, 18 | id: string, 19 | props: StackProps | undefined, 20 | buildConfig: BuildConfig, 21 | ) { 22 | super(scope, id, props); 23 | 24 | new CrossAccountRole( 25 | this, 26 | name(buildConfig, "waiterHandler-ssoapi-Role"), 27 | buildConfig, 28 | { 29 | assumeAccountID: buildConfig.PipelineSettings.TargetAccountId, 30 | roleNameKey: "waiterHandler-ssoapi", 31 | policyStatement: new PolicyStatement({ 32 | resources: ["*"], 33 | actions: [ 34 | "sso:DescribeAccountAssignmentDeletionStatus", 35 | "sso:DescribePermissionSetProvisioningStatus", 36 | "sso:DescribeAccountAssignmentCreationStatus", 37 | ], 38 | }), 39 | }, 40 | ); 41 | 42 | new CrossAccountRole( 43 | this, 44 | name(buildConfig, "permissionSetHandler-ssoapi-Role"), 45 | buildConfig, 46 | { 47 | assumeAccountID: buildConfig.PipelineSettings.TargetAccountId, 48 | roleNameKey: "permissionSetHandler-ssoapi", 49 | policyStatement: new PolicyStatement({ 50 | resources: ["*"], 51 | actions: [ 52 | "sso:DeleteInlinePolicyFromPermissionSet", 53 | "sso:DeletePermissionSet", 54 | "sso:CreatePermissionSet", 55 | "sso:GetPermissionSet", 56 | "sso:TagResource", 57 | "sso:ListPermissionSetProvisioningStatus", 58 | "sso:UntagResource", 59 | "sso:ListInstances", 60 | "sso:DescribePermissionSet", 61 | "sso:ProvisionPermissionSet", 62 | "sso:ListPermissionSets", 63 | "sso:ListAccountsForProvisionedPermissionSet", 64 | "sso:PutInlinePolicyToPermissionSet", 65 | "sso:UpdatePermissionSet", 66 | "sso:GetInlinePolicyForPermissionSet", 67 | "sso:DescribePermissionSetProvisioningStatus", 68 | "sso:PutPermissionsBoundaryToPermissionSet", 69 | "sso:DeletePermissionsBoundaryFromPermissionSet", 70 | "sso:GetPermissionsBoundaryForPermissionSet", 71 | ], 72 | }), 73 | }, 74 | ); 75 | 76 | new CrossAccountRole( 77 | this, 78 | name(buildConfig, "linkManagerHandler-ssoapi-role"), 79 | buildConfig, 80 | { 81 | assumeAccountID: buildConfig.PipelineSettings.TargetAccountId, 82 | roleNameKey: "linkManagerHandler-ssoapi", 83 | policyStatement: new PolicyStatement({ 84 | resources: ["*"], 85 | actions: [ 86 | "sso:CreateAccountAssignment", 87 | "sso:DescribeAccountAssignmentDeletionStatus", 88 | "sso:ListAccountAssignmentDeletionStatus", 89 | "sso:DescribeAccountAssignmentCreationStatus", 90 | "sso:DeleteAccountAssignment", 91 | "sso:ListAccountAssignmentCreationStatus", 92 | "sso:ListAccountAssignments", 93 | "sso:ListInstances", 94 | ], 95 | }), 96 | }, 97 | ); 98 | 99 | new CrossAccountRole( 100 | this, 101 | name(buildConfig, "listInstances-ssoapi-role"), 102 | buildConfig, 103 | { 104 | assumeAccountID: buildConfig.PipelineSettings.TargetAccountId, 105 | roleNameKey: "listInstances-ssoapi", 106 | policyStatement: new PolicyStatement({ 107 | resources: ["*"], 108 | actions: ["sso:ListInstances"], 109 | }), 110 | }, 111 | ); 112 | 113 | new CrossAccountRole( 114 | this, 115 | name(buildConfig, "listPrincipals-identitystoreapi-role"), 116 | buildConfig, 117 | { 118 | assumeAccountID: buildConfig.PipelineSettings.TargetAccountId, 119 | roleNameKey: "listPrincipals-identitystoreapi", 120 | policyStatement: new PolicyStatement({ 121 | resources: ["*"], 122 | actions: [ 123 | "identitystore:ListGroups", 124 | "identitystore:ListUsers", 125 | "identitystore:DescribeGroup", 126 | "identitystore:DescribeUser", 127 | ], 128 | }), 129 | }, 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/stacks/pipelineStageStacks/sso-events-processor.ts: -------------------------------------------------------------------------------- 1 | /** Deploys event bridge rules for SSO events in SSO account */ 2 | import { Rule, RuleTargetInput } from "aws-cdk-lib/aws-events"; 3 | import { SnsTopic } from "aws-cdk-lib/aws-events-targets"; 4 | import { 5 | AccountPrincipal, 6 | PolicyStatement, 7 | ServicePrincipal, 8 | } from "aws-cdk-lib/aws-iam"; 9 | import { Key } from "aws-cdk-lib/aws-kms"; 10 | import { Topic } from "aws-cdk-lib/aws-sns"; 11 | import { Stack, StackProps } from "aws-cdk-lib"; 12 | import { Construct } from "constructs"; 13 | import { BuildConfig } from "../../build/buildConfig"; 14 | import { SSMParamWriter } from "../../constructs/ssm-param-writer"; 15 | 16 | function name(buildConfig: BuildConfig, resourcename: string): string { 17 | return buildConfig.Environment + "-" + resourcename; 18 | } 19 | 20 | export class SSOEventsProcessor extends Stack { 21 | constructor( 22 | scope: Construct, 23 | id: string, 24 | props: StackProps | undefined, 25 | buildConfig: BuildConfig, 26 | ) { 27 | super(scope, id, props); 28 | 29 | const ssoArtefactsKey = new Key( 30 | this, 31 | name(buildConfig, "ssoArtefactsKey"), 32 | { 33 | enableKeyRotation: true, 34 | alias: name(buildConfig, "ssoArtefactsKey"), 35 | }, 36 | ); 37 | 38 | ssoArtefactsKey.grantEncryptDecrypt( 39 | new ServicePrincipal("events.amazonaws.com"), 40 | ); 41 | 42 | const ssoGroupEventsNotificationTopic = new Topic( 43 | this, 44 | name(buildConfig, "ssoGroupEventsNotificationTopic"), 45 | { 46 | masterKey: ssoArtefactsKey, 47 | displayName: name(buildConfig, "ssoGroupEventsNotificationTopic"), 48 | }, 49 | ); 50 | 51 | ssoGroupEventsNotificationTopic.addToResourcePolicy( 52 | new PolicyStatement({ 53 | actions: ["SNS:Subscribe", "SNS:Receive"], 54 | resources: ["*"], 55 | principals: [ 56 | new AccountPrincipal(buildConfig.PipelineSettings.TargetAccountId), 57 | ], 58 | }), 59 | ); 60 | 61 | new SSMParamWriter( 62 | this, 63 | name(buildConfig, "ssoGroupEventsNotificationTopicArn"), 64 | buildConfig, 65 | { 66 | ParamNameKey: "ssoGroupEventsNotificationTopicArn", 67 | ParamValue: ssoGroupEventsNotificationTopic.topicArn, 68 | ReaderAccountId: buildConfig.PipelineSettings.TargetAccountId, 69 | }, 70 | ); 71 | 72 | const ssoGroupHandlerTrigger = new Rule( 73 | this, 74 | name(buildConfig, "ssoGroupHandlerTrigger"), 75 | { 76 | description: "Process SCIM group changes", 77 | enabled: true, 78 | eventPattern: { 79 | account: [this.account], 80 | detail: { 81 | eventSource: ["sso-directory.amazonaws.com"], 82 | eventName: ["CreateGroup", "DeleteGroup"], 83 | errorCode: [{ exists: false }], 84 | }, 85 | }, 86 | ruleName: name(buildConfig, "ssoGroupHandler"), 87 | }, 88 | ); 89 | 90 | ssoGroupHandlerTrigger.addTarget( 91 | new SnsTopic(ssoGroupEventsNotificationTopic, { 92 | message: RuleTargetInput, 93 | }), 94 | ); 95 | 96 | const ssoUserEventsNotificationTopic = new Topic( 97 | this, 98 | name(buildConfig, "ssoUserEventsNotificationTopic"), 99 | { 100 | masterKey: ssoArtefactsKey, 101 | displayName: name(buildConfig, "ssoUserEventsNotificationTopic"), 102 | }, 103 | ); 104 | 105 | ssoUserEventsNotificationTopic.addToResourcePolicy( 106 | new PolicyStatement({ 107 | actions: ["SNS:Subscribe", "SNS:Receive"], 108 | resources: ["*"], 109 | principals: [ 110 | new AccountPrincipal(buildConfig.PipelineSettings.TargetAccountId), 111 | ], 112 | }), 113 | ); 114 | 115 | new SSMParamWriter( 116 | this, 117 | name(buildConfig, "ssoUserEventsNotificationTopicArn"), 118 | buildConfig, 119 | { 120 | ParamNameKey: "ssoUserEventsNotificationTopicArn", 121 | ParamValue: ssoUserEventsNotificationTopic.topicArn, 122 | ReaderAccountId: buildConfig.PipelineSettings.TargetAccountId, 123 | }, 124 | ); 125 | 126 | const ssoUserHandlerTrigger = new Rule( 127 | this, 128 | name(buildConfig, "ssoUserHandlerTrigger"), 129 | { 130 | description: "Process SCIM User changes", 131 | enabled: true, 132 | eventPattern: { 133 | account: [this.account], 134 | detail: { 135 | eventSource: ["sso-directory.amazonaws.com"], 136 | eventName: ["CreateUser", "DeleteUser"], 137 | errorCode: [{ exists: false }], 138 | }, 139 | }, 140 | ruleName: name(buildConfig, "ssoUserHandler"), 141 | }, 142 | ); 143 | 144 | ssoUserHandlerTrigger.addTarget( 145 | new SnsTopic(ssoUserEventsNotificationTopic, { 146 | message: RuleTargetInput, 147 | }), 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /lib/state-machines/import-current-config-asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "Source state machine to handle importing existing SSO configuration data into the solution", 3 | "StartAt": "Check trigger source?", 4 | "States": { 5 | "Check trigger source?": { 6 | "Type": "Choice", 7 | "Choices": [ 8 | { 9 | "And": [ 10 | { 11 | "Variable": "$.eventType", 12 | "StringEquals": "Delete" 13 | }, 14 | { 15 | "Variable": "$.triggerSource", 16 | "StringEquals": "CloudFormation" 17 | } 18 | ], 19 | "Next": "Catchall" 20 | } 21 | ], 22 | "Default": "ListInstances" 23 | }, 24 | "Catchall": { 25 | "Type": "Pass", 26 | "End": true 27 | }, 28 | "ListInstances": { 29 | "Type": "Task", 30 | "Next": "Create Temporary permission sets table", 31 | "Parameters": {}, 32 | "Resource": "arn:aws:states:::aws-sdk:ssoadmin:listInstances", 33 | "ResultPath": "$.listInstancesResult", 34 | "Retry": [ 35 | { 36 | "ErrorEquals": ["States.TaskFailed"], 37 | "BackoffRate": 1.5, 38 | "IntervalSeconds": 2, 39 | "MaxAttempts": 2 40 | } 41 | ], 42 | "Catch": [ 43 | { 44 | "ErrorEquals": ["States.ALL"], 45 | "Next": "Catchall", 46 | "ResultPath": "$.solutionError" 47 | } 48 | ] 49 | }, 50 | "Create Temporary permission sets table": { 51 | "Type": "Task", 52 | "Next": "Wait for temporary permission sets table creation", 53 | "Parameters": { 54 | "AttributeDefinitions": [ 55 | { 56 | "AttributeName": "psArn", 57 | "AttributeType": "S" 58 | } 59 | ], 60 | "KeySchema": [ 61 | { 62 | "AttributeName": "psArn", 63 | "KeyType": "HASH" 64 | } 65 | ], 66 | "TableName.$": "$.temporaryPermissionSetTableName", 67 | "BillingMode": "PAY_PER_REQUEST" 68 | }, 69 | "Resource": "arn:aws:states:::aws-sdk:dynamodb:createTable", 70 | "ResultPath": "$.createPermissionSetsTableResult", 71 | "Retry": [ 72 | { 73 | "ErrorEquals": ["States.TaskFailed"], 74 | "BackoffRate": 1.5, 75 | "IntervalSeconds": 2, 76 | "MaxAttempts": 2 77 | } 78 | ], 79 | "Catch": [ 80 | { 81 | "ErrorEquals": ["States.ALL"], 82 | "Next": "Catchall", 83 | "ResultPath": "$.solutionError" 84 | } 85 | ] 86 | }, 87 | "Wait for temporary permission sets table creation": { 88 | "Type": "Wait", 89 | "SecondsPath": "$.waitSeconds", 90 | "Next": "Get temporary permission set table creation status" 91 | }, 92 | "Get temporary permission set table creation status": { 93 | "Type": "Task", 94 | "Next": "Verify if temporary permission sets table is created", 95 | "Parameters": { 96 | "TableName.$": "$.temporaryPermissionSetTableName" 97 | }, 98 | "Resource": "arn:aws:states:::aws-sdk:dynamodb:describeTable", 99 | "ResultPath": "$.describeTemporaryPermissionSetsTableResult", 100 | "Retry": [ 101 | { 102 | "ErrorEquals": ["States.TaskFailed"], 103 | "BackoffRate": 1.5, 104 | "IntervalSeconds": 2, 105 | "MaxAttempts": 2 106 | } 107 | ], 108 | "Catch": [ 109 | { 110 | "ErrorEquals": ["States.ALL"], 111 | "Next": "Delete temporary permission sets table", 112 | "ResultPath": "$.solutionError" 113 | } 114 | ] 115 | }, 116 | "Verify if temporary permission sets table is created": { 117 | "Type": "Choice", 118 | "Choices": [ 119 | { 120 | "Variable": "$.describeTemporaryPermissionSetsTableResult.Table.TableStatus", 121 | "StringEquals": "ACTIVE", 122 | "Next": "Trigger Import Permission Set state machine" 123 | } 124 | ], 125 | "Default": "Repeat wait loop until permission set table is created" 126 | }, 127 | "Repeat wait loop until permission set table is created": { 128 | "Type": "Wait", 129 | "SecondsPath": "$.waitSeconds", 130 | "Next": "Get temporary permission set table creation status" 131 | }, 132 | "Trigger Import Permission Set state machine": { 133 | "Type": "Task", 134 | "Resource": "arn:aws:states:::states:startExecution.sync:2", 135 | "Parameters": { 136 | "StateMachineArn.$": "$.importPermissionSetSMArn", 137 | "Input": { 138 | "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id", 139 | "instanceArn.$": "$.listInstancesResult.Instances[0].InstanceArn", 140 | "identityStoreId.$": "$.listInstancesResult.Instances[0].IdentityStoreId", 141 | "ps-importTopicArn.$": "$.permissionSetImportTopicArn", 142 | "ps-tableName.$": "$.temporaryPermissionSetTableName", 143 | "accounts-stateMachineArn.$": "$.importAccountAssignmentSMArn", 144 | "accounts-importTopicArn.$": "$.accountAssignmentImportTopicArn", 145 | "requestId.$": "$.requestId", 146 | "triggerSource.$": "$.triggerSource", 147 | "waitSeconds.$": "$.waitSeconds", 148 | "pageSize.$": "$.pageSize", 149 | "importCmpAndPbArn.$": "$.importCmpAndPbArn" 150 | } 151 | }, 152 | "Next": "Delete temporary permission sets table", 153 | "ResultPath": null, 154 | "Catch": [ 155 | { 156 | "ErrorEquals": ["States.ALL"], 157 | "Next": "Delete temporary permission sets table", 158 | "ResultPath": "$.solutionError" 159 | } 160 | ] 161 | }, 162 | "Delete temporary permission sets table": { 163 | "Type": "Task", 164 | "Parameters": { 165 | "TableName.$": "$.temporaryPermissionSetTableName" 166 | }, 167 | "Resource": "arn:aws:states:::aws-sdk:dynamodb:deleteTable", 168 | "ResultPath": null, 169 | "Retry": [ 170 | { 171 | "ErrorEquals": ["States.TaskFailed"], 172 | "BackoffRate": 1.5, 173 | "IntervalSeconds": 2, 174 | "MaxAttempts": 2 175 | } 176 | ], 177 | "Next": "Check if StateMachine succeeded or failed?" 178 | }, 179 | "Check if StateMachine succeeded or failed?": { 180 | "Type": "Choice", 181 | "Choices": [ 182 | { 183 | "Variable": "$.solutionError", 184 | "IsPresent": true, 185 | "Next": "State machine failed" 186 | } 187 | ], 188 | "Default": "State machine succeeded" 189 | }, 190 | "State machine failed": { 191 | "Type": "Fail", 192 | "Error": "Import Current AWS IAM Identity Center configuration failed", 193 | "Cause": "Import Current AWS IAM Identity Center configuration failed" 194 | }, 195 | "State machine succeeded": { 196 | "Type": "Pass", 197 | "End": true 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-sso-extensions-for-enterprise", 3 | "version": "3.1.9", 4 | "bin": { 5 | "aws-sso-extensions-for-enterprise": "bin/aws-sso-extensions-for-enterprise.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "prettier:check": "prettier --check .", 13 | "prettier": "prettier -w .", 14 | "lint:check": "eslint . --ignore-path .gitignore --max-warnings 0", 15 | "lint": "eslint . --fix --ignore-path .gitignore --max-warnings 0", 16 | "cdk-synth-env": "cdk synth env-aws-sso-extensions-for-enterprise -c config=env", 17 | "cdk-deploy-env": "cdk deploy env-aws-sso-extensions-for-enterprise -c config=env", 18 | "cdk-destroy-env": "cdk destroy env-aws-sso-extensions-for-enterprise -c config=env", 19 | "synth-region-switch-discover": "cdk synth aws-sso-extensions-region-switch-discover -c config=region-switch-discover", 20 | "deploy-region-switch-discover": "cdk deploy aws-sso-extensions-region-switch-discover -c config=region-switch-discover", 21 | "destroy-region-switch-discover": "cdk destroy aws-sso-extensions-region-switch-discover -c config=region-switch-discover", 22 | "synth-region-switch-deploy": "cdk synth aws-sso-extensions-region-switch-deploy -c config=region-switch-deploy", 23 | "deploy-region-switch": "cdk deploy aws-sso-extensions-region-switch-deploy -c config=region-switch-deploy", 24 | "destroy-region-switch-deploy": "cdk destroy aws-sso-extensions-region-switch-deploy -c config=region-switch-deploy", 25 | "postinstall": "yarn install --cwd ./lib/lambda-functions --frozen-lockfile" 26 | }, 27 | "devDependencies": { 28 | "@types/aws-lambda": "^8.10.142", 29 | "@types/jest": "^29.5.12", 30 | "@types/js-yaml": "^4.0.9", 31 | "@types/node": "^22.0.0", 32 | "@types/uuid": "^10.0.0", 33 | "@typescript-eslint/eslint-plugin": "^7.18.0", 34 | "@typescript-eslint/parser": "^7.18.0", 35 | "aws-cdk": "^2.160.0", 36 | "esbuild": "^0.23.0", 37 | "eslint": "^8.56.0", 38 | "eslint-plugin-import": "^2.29.1", 39 | "eslint-plugin-security": "^3.0.1", 40 | "jest": "^29.7.0", 41 | "prettier": "^3.3.3", 42 | "prettier-plugin-jsdoc": "^1.3.0", 43 | "ts-jest": "^29.2.3", 44 | "ts-node": "^10.9.2", 45 | "typescript": "^5.5.4", 46 | "uuid": "^10.0.0" 47 | }, 48 | "dependencies": { 49 | "aws-cdk-lib": "^2.160.0", 50 | "constructs": "^10.3.0", 51 | "js-yaml": "^4.1.0", 52 | "source-map-support": "^0.5.21" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018", "DOM"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "resolveJsonModule": true, 21 | "typeRoots": ["./node_modules/@types"] 22 | }, 23 | "exclude": ["node_modules", "cdk.out"] 24 | } 25 | --------------------------------------------------------------------------------