├── .idea └── .gitignore ├── diagram └── architecture_diagram.png ├── CODE_OF_CONDUCT.md ├── src ├── codebuild │ ├── buildspec-mapping.yml │ ├── buildspec-validation.yml │ ├── buildspec-zipfiles.yml │ └── buildspec-param.yml ├── automation-code │ ├── identity-center-auto-assign │ │ └── cfnresponse.py │ ├── identity-center-auto-permissionsets │ │ └── cfnresponse.py │ └── permission-set-and-mapping-files-generator │ │ └── auto-generate-permissionsets-mapping-files.py └── validation │ └── syntax-validator.py ├── identity-center-mapping-info ├── permission-sets │ ├── 2-example-readonly.json │ ├── 1-example-admin.json │ ├── 6-example-cx-managed-boundary.json │ ├── 7-example-aws-managed-boundary.json │ ├── 3-example-billing.json │ ├── 5-example-sec-readonly.json │ └── 4-example-operations.json ├── global-mapping.json └── target-mapping.json ├── identity-center-stacks-parameters.json ├── .gitignore ├── LICENSE ├── templates ├── management-account-org-events-forwarder.template ├── delegate-admin │ ├── IC_delegate_admin_main.py │ └── IC-Delegate-Admin.template ├── identity-center-s3-bucket.template ├── codepipeline-stack.template └── identity-center-automation.template ├── CONTRIBUTING.md └── CHANGELOG.md /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /diagram/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-identitycenter-codepipeline-auto-assignment/HEAD/diagram/architecture_diagram.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /src/codebuild/buildspec-mapping.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | git-credential-helper: yes 5 | phases: 6 | build: 7 | commands: 8 | - echo "Sync mapping files to S3 secure bucket" 9 | - pwd 10 | - ls -lah 11 | - aws s3 sync --delete identity-center-mapping-info/ s3://$S3_BUCKET_NAME/ --exclude "identity-center-auto-assign.zip" --exclude "identity-center-auto-permissionsets.zip" 12 | - aws s3api list-objects-v2 --bucket $S3_BUCKET_NAME 13 | - echo "Sync done." -------------------------------------------------------------------------------- /identity-center-mapping-info/permission-sets/2-example-readonly.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "2-example-readonly", 3 | "Description": "2-example-readonly", 4 | "Tags": [ 5 | { 6 | "Key": "identity-center-solution", 7 | "Value": "test" 8 | }, 9 | { 10 | "Key": "scope", 11 | "Value": "global" 12 | } 13 | ], 14 | "ManagedPolicies": [ 15 | { 16 | "Name": "SecurityAudit", 17 | "Arn": "arn:aws:iam::aws:policy/SecurityAudit" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /identity-center-mapping-info/permission-sets/1-example-admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "1-example-admin", 3 | "Description": "1-example-admin", 4 | "Session_Duration": "PT12H", 5 | "Tags": [ 6 | { 7 | "Key": "identity-center-solution", 8 | "Value": "test" 9 | }, 10 | { 11 | "Key": "scope", 12 | "Value": "global" 13 | } 14 | ], 15 | "ManagedPolicies": [ 16 | { 17 | "Name": "AdministratorAccess", 18 | "Arn": "arn:aws:iam::aws:policy/AdministratorAccess" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /identity-center-mapping-info/global-mapping.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "GlobalGroupName": "Example_Group_1", 4 | "PermissionSetName": [ 5 | "1-example-admin" 6 | ], 7 | "Target": "Global" 8 | }, 9 | { 10 | "GlobalGroupName": "Example_Group_2", 11 | "PermissionSetName": [ 12 | "2-example-readonly" 13 | ], 14 | "Target": "Global" 15 | }, 16 | { 17 | "GlobalGroupName": "Example_Group_3", 18 | "PermissionSetName": [ 19 | "3-example-billing" 20 | ], 21 | "Target": "Global" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /identity-center-stacks-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "AdminDelegated": "true", 4 | "ControlTowerEnabled": "true", 5 | "OrgManagementAccount": "", 6 | "OrganizationId": "", 7 | "IdentityStoreId": "", 8 | "ICInstanceARN": "", 9 | "ICMappingBucketName": "ic-sso-automation", 10 | "SNSEmailEndpointSubscription": "", 11 | "createICAdminRole": "true", 12 | "ICAutomationAdminArn": "", 13 | "createICKMSAdminRole": "true", 14 | "ICKMSAdminArn": "", 15 | "createS3KmsKey": "true", 16 | "S3KmsArn": "", 17 | "BuildTimeout": "120" 18 | } 19 | } -------------------------------------------------------------------------------- /identity-center-mapping-info/permission-sets/6-example-cx-managed-boundary.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "6-example-cx-managed-boundary", 3 | "Description": "Example permission set with customer managed boundary policy", 4 | "Session_Duration": "PT12H", 5 | "Tags": [ 6 | { 7 | "Key": "identity-center-solution", 8 | "Value": "test" 9 | } 10 | ], 11 | "ManagedPolicies": [ 12 | { 13 | "Name": "ViewOnlyAccess", 14 | "Arn": "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" 15 | } 16 | ], 17 | "PermissionsBoundary": { 18 | "Name": "testPermissionBoundary", 19 | "Path": "/" 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | 4 | # VS Code 5 | .vscode 6 | 7 | # IDEA 8 | # https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 9 | 10 | # User-specific stuff 11 | .idea/**/workspace.xml 12 | .idea/**/tasks.xml 13 | .idea/**/usage.statistics.xml 14 | .idea/**/dictionaries 15 | .idea/**/shelf 16 | 17 | # AWS User-specific 18 | .idea/**/aws.xml 19 | 20 | # Generated files 21 | .idea/**/contentModel.xml 22 | 23 | # Sensitive or high-churn files 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | .idea/**/dbnavigator.xml 31 | 32 | # Testing stuff 33 | */Optimized/ -------------------------------------------------------------------------------- /identity-center-mapping-info/permission-sets/7-example-aws-managed-boundary.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "7-example-aws-managed-boundary", 3 | "Description": "Example permission set with AWS managed boundary policy", 4 | "Session_Duration": "PT12H", 5 | "Tags": [ 6 | { 7 | "Key": "identity-center-solution", 8 | "Value": "test" 9 | } 10 | ], 11 | "ManagedPolicies": [ 12 | { 13 | "Name": "PowerUserAccess", 14 | "Arn": "arn:aws:iam::aws:policy/PowerUserAccess" 15 | } 16 | ], 17 | "PermissionsBoundary": { 18 | "Name": "ViewOnlyAccess", 19 | "Arn": "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" 20 | } 21 | } -------------------------------------------------------------------------------- /identity-center-mapping-info/permission-sets/3-example-billing.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "3-example-billing", 3 | "Description": "3-example-billing", 4 | "Tags": [ 5 | { 6 | "Key": "identity-center-solution", 7 | "Value": "test" 8 | }, 9 | { 10 | "Key": "scope", 11 | "Value": "global" 12 | } 13 | ], 14 | "ManagedPolicies": [], 15 | "InlinePolicies": { 16 | "Version": "2012-10-17", 17 | "Statement": [ 18 | { 19 | "Sid": "BillingReader", 20 | "Effect": "Allow", 21 | "Action": [ 22 | "budgets:ViewBudget" 23 | ], 24 | "Resource": "*" 25 | } 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /identity-center-mapping-info/permission-sets/5-example-sec-readonly.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "5-example-sec-readonly", 3 | "Description": "5-example-security-readonly", 4 | "Tags": [ 5 | { 6 | "Key": "identity-center-solution", 7 | "Value": "test" 8 | }, 9 | { 10 | "Key": "scope", 11 | "Value": "individual" 12 | } 13 | ], 14 | "ManagedPolicies": [ 15 | { 16 | "Name": "CloudWatchEventsReadOnlyAccess", 17 | "Arn": "arn:aws:iam::aws:policy/CloudWatchEventsReadOnlyAccess" 18 | }, 19 | { 20 | "Name": "AWSSupportAccess", 21 | "Arn": "arn:aws:iam::aws:policy/AWSSupportAccess" 22 | } 23 | ], 24 | "CustomerPolicies": [ 25 | { 26 | "Name": "customer-managed-policy-1", 27 | "Path": "/IAM-path-example/" 28 | }, 29 | { 30 | "Name": "customer-managed-policy-2", 31 | "Path": "/" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /identity-center-mapping-info/target-mapping.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "TargetGroupName":"Example_Group_4", 4 | "PermissionSetName":[ 5 | "4-example-operations" 6 | ], 7 | "Target": [ 8 | { 9 | "OrganizationalUnits": [ 10 | "Sandbox" 11 | ] 12 | } 13 | ] 14 | }, 15 | { 16 | "TargetGroupName":"Example_Group_5", 17 | "PermissionSetName":[ 18 | "5-example-sec-readonly" 19 | ], 20 | "Target": [ 21 | { 22 | "Accounts": [ 23 | "Audit" 24 | ] 25 | }, 26 | "098765432101" 27 | ] 28 | }, 29 | { 30 | "TargetGroupName":"Example_Group_1", 31 | "PermissionSetName":[ 32 | "6-example-cx-managed-policy-boundary" 33 | ], 34 | "Target": [ 35 | { 36 | "OrganizationalUnits": [ 37 | "Infrastructure/Infra-Dev", 38 | "Development" 39 | ] 40 | }, 41 | { 42 | "Accounts": [ 43 | "Shared_Services" 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "TargetGroupName":"Example_Group_2", 50 | "PermissionSetName":[ 51 | "7-example-aws-managed-policy-boundary" 52 | ], 53 | "Target":[ 54 | "384142793104" 55 | ] 56 | } 57 | ] -------------------------------------------------------------------------------- /src/automation-code/identity-center-auto-assign/cfnresponse.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | from __future__ import print_function 4 | import urllib3 5 | import json 6 | SUCCESS = "SUCCESS" 7 | FAILED = "FAILED" 8 | http = urllib3.PoolManager() 9 | 10 | 11 | def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None): 12 | responseUrl = event['ResponseURL'] 13 | print(responseUrl) 14 | responseBody = { 15 | 'Status' : responseStatus, 16 | 'Reason' : reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name), 17 | 'PhysicalResourceId' : physicalResourceId or context.log_stream_name, 18 | 'StackId' : event['StackId'], 19 | 'RequestId' : event['RequestId'], 20 | 'LogicalResourceId' : event['LogicalResourceId'], 21 | 'NoEcho' : noEcho, 22 | 'Data' : responseData 23 | } 24 | json_responseBody = json.dumps(responseBody) 25 | print("Response body:") 26 | print(json_responseBody) 27 | headers = { 28 | 'content-type' : '', 29 | 'content-length' : str(len(json_responseBody)) 30 | } 31 | try: 32 | response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody) 33 | print("Status code:", response.status) 34 | except Exception as e: 35 | print("send(..) failed executing http.request(..):", e) -------------------------------------------------------------------------------- /src/automation-code/identity-center-auto-permissionsets/cfnresponse.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | from __future__ import print_function 4 | import urllib3 5 | import json 6 | SUCCESS = "SUCCESS" 7 | FAILED = "FAILED" 8 | http = urllib3.PoolManager() 9 | 10 | 11 | def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None): 12 | responseUrl = event['ResponseURL'] 13 | print(responseUrl) 14 | responseBody = { 15 | 'Status' : responseStatus, 16 | 'Reason' : reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name), 17 | 'PhysicalResourceId' : physicalResourceId or context.log_stream_name, 18 | 'StackId' : event['StackId'], 19 | 'RequestId' : event['RequestId'], 20 | 'LogicalResourceId' : event['LogicalResourceId'], 21 | 'NoEcho' : noEcho, 22 | 'Data' : responseData 23 | } 24 | json_responseBody = json.dumps(responseBody) 25 | print("Response body:") 26 | print(json_responseBody) 27 | headers = { 28 | 'content-type' : '', 29 | 'content-length' : str(len(json_responseBody)) 30 | } 31 | try: 32 | response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody) 33 | print("Status code:", response.status) 34 | except Exception as e: 35 | print("send(..) failed executing http.request(..):", e) -------------------------------------------------------------------------------- /identity-center-mapping-info/permission-sets/4-example-operations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "4-example-operations", 3 | "Description": "4-example-operations", 4 | "Tags": [ 5 | { 6 | "Key": "identity-center-solution", 7 | "Value": "test" 8 | }, 9 | { 10 | "Key": "scope", 11 | "Value": "global" 12 | } 13 | ], 14 | "ManagedPolicies": [ 15 | { 16 | "Name": "SupportUser", 17 | "Arn": "arn:aws:iam::aws:policy/job-function/SupportUser" 18 | }, 19 | { 20 | "Name": "CloudWatchLogsReadOnlyAccess", 21 | "Arn": "arn:aws:iam::aws:policy/CloudWatchLogsReadOnlyAccess" 22 | } 23 | ], 24 | "InlinePolicies": { 25 | "Version": "2012-10-17", 26 | "Statement": [ 27 | { 28 | "Sid": "GlobalOpsSupportCustom", 29 | "Effect": "Allow", 30 | "Action": [ 31 | "ec2:*", 32 | "kms:DescribeKey" 33 | ], 34 | "Resource": "*" 35 | }, 36 | { 37 | "Sid": "Statement2", 38 | "Effect": "Allow", 39 | "Action": [ 40 | "apigateway:GET", 41 | "apigateway:DELETE" 42 | ], 43 | "Resource": [ 44 | "arn:aws:apigateway:us-west-2::/apis" 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/codebuild/buildspec-validation.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | env: 3 | shell: bash 4 | variables: 5 | EXIT_CODE: 0 6 | VALIDATION_FUNCTION_EXIT_CODE: 0 7 | ERROR_MESSAGE: "" 8 | phases: 9 | pre_build: 10 | commands: 11 | - echo "Installing dependencies" 12 | - apt update 13 | - apt-get install python3-pip -y -q 14 | - pip install -q boto3 botocore 15 | - echo 'The source artifact location is:' $CODEBUILD_SOURCE_VERSION 16 | - echo 'The extracted source artifact bucket name is:' $CODEBUILD_SOURCE_VERSION | awk -F'/' '{print $1}' | awk -F':::' '{print $2}' 17 | - export ARTIFACT_S3_BUCKET_NAME=$(echo "$CODEBUILD_SOURCE_VERSION" | awk -F'/' '{print $1}' | awk -F':::' '{print $2}') 18 | build: 19 | commands: 20 | - echo "Starting validation phase" 21 | - python3 src/validation/syntax-validator.py || VALIDATION_FUNCTION_EXIT_CODE=$? 22 | - | 23 | if [ $VALIDATION_FUNCTION_EXIT_CODE -ne 0 ]; then 24 | ERROR_MESSAGE="[ERROR] Validation function (syntax_validator.py) failed with exit code $VALIDATION_FUNCTION_EXIT_CODE :( Please see function logs above or in CloudWatch for details" 25 | EXIT_CODE=$VALIDATION_FUNCTION_EXIT_CODE 26 | fi 27 | if [ -n "$ERROR_MESSAGE" ]; then 28 | echo "$ERROR_MESSAGE" 29 | else 30 | echo "Validation function (syntax_validator.py) ran successfully. Execution is now complete :)" 31 | fi 32 | - echo 'Exiting build with final exit code:' $EXIT_CODE 33 | - exit $EXIT_CODE 34 | post_build: 35 | commands: 36 | - echo "Validation Action complete" -------------------------------------------------------------------------------- /src/codebuild/buildspec-zipfiles.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | git-credential-helper: yes 5 | phases: 6 | pre_build: 7 | commands: 8 | - apt-get install jq 9 | build: 10 | commands: 11 | - echo "Sync source code to S3 bucket" 12 | - pwd 13 | - ls -lah 14 | #Build source code Zip file with no folder structure 15 | - zip -j identity-center-auto-assign.zip src/automation-code/identity-center-auto-assign/*.py 16 | - zip -j identity-center-auto-permissionsets.zip src/automation-code/identity-center-auto-permissionsets/*.py 17 | - ls -lah 18 | #Upload automation source zip code using aws sync. The bucket name is defined in identity-center-s3-bucket.template 19 | - aws s3 sync . s3://$S3_BUCKET_NAME/ --exclude "*" --include "identity-center-auto-assign.zip" 20 | - aws s3 sync . s3://$S3_BUCKET_NAME/ --exclude "*" --include "identity-center-auto-permissionsets.zip" 21 | - aws s3api list-objects-v2 --bucket $S3_BUCKET_NAME 22 | - IC_AUTO_ASSIGN_VERSION_ID=$(aws s3api put-object-tagging --bucket $S3_BUCKET_NAME --key identity-center-auto-assign.zip --tagging 'TagSet=[{Key=version,Value=latest}]' --output text) 23 | - IC_AUTO_PERMISSIONS_VERSION_ID=$(aws s3api put-object-tagging --bucket $S3_BUCKET_NAME --key identity-center-auto-permissionsets.zip --tagging 'TagSet=[{Key=version,Value=latest}]' --output text) 24 | - jq --arg AssignmentAutomationZipFileVersion "$IC_AUTO_ASSIGN_VERSION_ID" '.Parameters.AssignmentAutomationZipFileVersion = $AssignmentAutomationZipFileVersion' ic-automation-parameters.json > temp.json && mv temp.json ic-automation-parameters.json 25 | - jq --arg PermissionSetsAutomationZipFileVersion "$IC_AUTO_PERMISSIONS_VERSION_ID" '.Parameters.PermissionSetsAutomationZipFileVersion = $PermissionSetsAutomationZipFileVersion' ic-automation-parameters.json > temp.json && mv temp.json ic-automation-parameters.json 26 | - cat ic-automation-parameters.json 27 | 28 | artifacts: 29 | files: 30 | - '**/*' -------------------------------------------------------------------------------- /templates/management-account-org-events-forwarder.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Template to forward Organization events from management account to Identity Center delegated admin account (uksb-8ggfy7rjoj)." 3 | Parameters: 4 | IdcDelegatedAccountId: 5 | Type: String 6 | Description: The account ID of the delegated admin account for IAM Identity Center where events should be forwarded (uksb-8ggfy7rjoj). 7 | TargetRegion: 8 | Type: String 9 | Description: The region where the Identity Center solution is deployed in the delegated admin account 10 | 11 | Resources: 12 | OrgEventsForwardingRole: 13 | Type: AWS::IAM::Role 14 | Properties: 15 | RoleName: OrgEventsForwardingRole 16 | AssumeRolePolicyDocument: 17 | Version: "2012-10-17" 18 | Statement: 19 | - Effect: Allow 20 | Principal: 21 | Service: events.amazonaws.com 22 | Action: sts:AssumeRole 23 | Policies: 24 | - PolicyName: EventBridgeForwardingPolicy 25 | PolicyDocument: 26 | Version: "2012-10-17" 27 | Statement: 28 | - Effect: Allow 29 | Action: events:PutEvents 30 | Resource: !Sub arn:aws:events:${TargetRegion}:${IdcDelegatedAccountId}:event-bus/ManagementAccountOrgEvents-${IdcDelegatedAccountId} 31 | 32 | OrgEventsRule: 33 | Type: AWS::Events::Rule 34 | Properties: 35 | Description: Forward Organization events to member account 36 | EventPattern: 37 | source: 38 | - aws.organizations 39 | detail-type: 40 | - AWS API Call via CloudTrail 41 | - AWS Service Event via CloudTrail 42 | detail: 43 | eventName: 44 | - CreateOrganizationalUnit 45 | - MoveAccount 46 | - CreateAccountResult 47 | - AcceptHandshake 48 | State: ENABLED 49 | Targets: 50 | - Arn: !Sub arn:aws:events:${TargetRegion}:${IdcDelegatedAccountId}:event-bus/ManagementAccountOrgEvents-${IdcDelegatedAccountId} 51 | RoleArn: !GetAtt OrgEventsForwardingRole.Arn 52 | Id: ForwardToMemberAccount -------------------------------------------------------------------------------- /src/codebuild/buildspec-param.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | env: 3 | git-credential-helper: yes 4 | phases: 5 | pre_build: 6 | commands: 7 | - export DEBIAN_FRONTEND=noninteractive 8 | - apt update 9 | - apt-get install python3-pip -y -q 10 | - apt-get install jq git ruby-rubygems -y -q 11 | - pip3 install cfn-lint --quiet 12 | - gem install cfn-nag 13 | - which cfn_nag_scan 14 | - echo 'The source artifact location is:' $CODEBUILD_SOURCE_VERSION 15 | - echo 'The extracted source artifact bucket name is:' $CODEBUILD_SOURCE_VERSION | awk -F'/' '{print $1}' | awk -F':::' '{print $2}' 16 | - export ARTIFACT_S3_BUCKET_NAME=$(echo "$CODEBUILD_SOURCE_VERSION" | awk -F'/' '{print $1}' | awk -F':::' '{print $2}') 17 | build: 18 | commands: 19 | - set -eux 20 | - echo "Create CloudFormation Parameter Files." 21 | - jq --arg artifact_bucket "$ARTIFACT_S3_BUCKET_NAME" '.Parameters.ArtifactBucketName = $artifact_bucket' identity-center-stacks-parameters.json > ic-automation-parameters.json 22 | - jq "del(.Parameters.OrganizationId, .Parameters.ICKMSAdminArn, .Parameters.createICKMSAdminRole, .Parameters.createICAdminRole, .Parameters.createS3KmsKey, .Parameters.S3KmsArn)" ic-automation-parameters.json > temp.json && mv temp.json ic-automation-parameters.json 23 | - jq "del(.Parameters.IdentityStoreId, .Parameters.SNSEmailEndpointSubscription, .Parameters.ICInstanceARN, .Parameters.OrgManagementAccount, .Parameters.AdminDelegated, .Parameters.ControlTowerEnabled, .Parameters.BuildTimeout)" identity-center-stacks-parameters.json > ic-s3-parameters.json 24 | - cat ic-automation-parameters.json 25 | - cat ic-s3-parameters.json 26 | - echo "Parameter Process is done." 27 | - ls -lah 28 | # CFN Template Linting and Security Scans 29 | - echo "Start scanning CloudFormation templates using cfn-nag Tool......." 30 | - cfn_nag_scan --input-path templates/codepipeline-stack.template 31 | - cfn_nag_scan --input-path templates/delegate-admin/IC-Delegate-Admin.template 32 | - cfn_nag_scan --input-path templates/identity-center-s3-bucket.template 33 | - cfn_nag_scan --input-path templates/identity-center-automation.template 34 | - cfn_nag_scan --input-path templates/management-account-org-events-forwarder.template 35 | 36 | artifacts: 37 | files: 38 | - '**/*' -------------------------------------------------------------------------------- /templates/delegate-admin/IC_delegate_admin_main.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import json 4 | import logging 5 | from time import sleep 6 | 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | 10 | 11 | def delegate_sso_admin(event, context): 12 | delegate = event["delegate"] 13 | logger.info(f'Input value for delegate {delegate}') 14 | if delegate.lower() != 'true': 15 | logger.info(f'Delegate: {delegate.lower()}. Delegation not requested') 16 | return 17 | 18 | account_id = event["account_id"] 19 | logger.info(f'Input value for account ID to delegate AWS IC to: {account_id}') 20 | 21 | org_client = boto3.client('organizations') 22 | 23 | # List the delegated administrators for IC 24 | admins = org_client.list_delegated_administrators( 25 | ServicePrincipal='sso.amazonaws.com' 26 | ) 27 | sleep(0.1) 28 | 29 | # Check if any other accounts are delegated admins for IC 30 | other_admins = [admin['Id'] 31 | for admin in admins['DelegatedAdministrators'] if admin['Id'] != account_id] 32 | if other_admins: 33 | # Deregister the other delegated admins for IC 34 | for admin_id in other_admins: 35 | logger.info('Deregistering other delegated administrators for AWS IC') 36 | org_client.deregister_delegated_administrator( 37 | AccountId=admin_id, 38 | ServicePrincipal='sso.amazonaws.com' 39 | ) 40 | logger.info(f'Deregistered {admin_id}') 41 | sleep(0.1) 42 | 43 | # Check if the specified account is already a delegated admin for IC 44 | found = False 45 | for admin in admins['DelegatedAdministrators']: 46 | if admin['Id'] == account_id: 47 | found = True 48 | logger.info(f'{account_id} is already a delegated administrator for AWS IC') 49 | break 50 | 51 | if not found: 52 | # Delegate the specified account as an administrator for IC 53 | org_client.register_delegated_administrator( 54 | AccountId=account_id, 55 | ServicePrincipal='sso.amazonaws.com' 56 | ) 57 | logger.info(f'Delegated {account_id} as administrator for AWS IC') 58 | sleep(0.1) 59 | 60 | 61 | def lambda_handler(event, context): 62 | logger.info(event) 63 | logger.debug(context) 64 | try: 65 | delegate_sso_admin(event, context) 66 | except Exception as e: 67 | logger.exception(e) 68 | -------------------------------------------------------------------------------- /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 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 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. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | 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. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | 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. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /templates/delegate-admin/IC-Delegate-Admin.template: -------------------------------------------------------------------------------- 1 | Parameters: 2 | accountid: 3 | Type: String 4 | Default: "123456789012" 5 | AllowedPattern: '^[0-9]{12}$' 6 | Description: >- 7 | Enter the AWS Account ID of the account that you'd like to delegate as administrator for Identity Center (uksb-8ggfy7rjoj). 8 | delegate: 9 | Type: String 10 | Default: "true" 11 | AllowedValues: 12 | - "true" 13 | - "false" 14 | Description: >- 15 | Set to true if you'd like to delegate another account as a delegated administrator for Identity Center 16 | Resources: 17 | icDelegateAdminLambdaExecutionRole: 18 | Type: AWS::IAM::Role 19 | Properties: 20 | AssumeRolePolicyDocument: 21 | Statement: 22 | - Action: sts:AssumeRole 23 | Effect: Allow 24 | Principal: 25 | Service: lambda.amazonaws.com 26 | Version: "2012-10-17" 27 | ManagedPolicyArns: 28 | - Fn::Join: 29 | - "" 30 | - - "arn:" 31 | - !Ref AWS::Partition 32 | - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 33 | icDelegateAdminPolicy: 34 | Type: AWS::IAM::Policy 35 | Properties: 36 | PolicyDocument: 37 | Statement: 38 | - Action: 39 | - logs:CreateLogGroup 40 | - logs:CreateLogStream 41 | - logs:PutLogEvents 42 | - organizations:DeregisterDelegatedAdministrator 43 | - organizations:ListDelegatedAdministrators 44 | - organizations:RegisterDelegatedAdministrator 45 | Effect: Allow 46 | Resource: "*" 47 | Version: "2012-10-17" 48 | PolicyName: icDelegateAdminPolicy 49 | Roles: 50 | - !Ref icDelegateAdminLambdaExecutionRole 51 | icDelegateAdminFunction: 52 | Type: AWS::Lambda::Function 53 | Properties: 54 | Code: 55 | ZipFile: | 56 | from __future__ import print_function 57 | import boto3 58 | import os 59 | import urllib3 60 | import json 61 | import logging 62 | from time import sleep 63 | 64 | SUCCESS = "SUCCESS" 65 | FAILED = "FAILED" 66 | 67 | logger = logging.getLogger() 68 | logger.setLevel(logging.INFO) 69 | 70 | http = urllib3.PoolManager() 71 | 72 | def delegate_sso_admin(event, context): 73 | delegate = event["ResourceProperties"]["delegate"] 74 | print(f'Input value for delegate {delegate}') 75 | if delegate.lower() != 'true': 76 | print(f'Delegate: {delegate.lower()}. Delegation not requested') 77 | return 78 | 79 | account_id = event["ResourceProperties"]["account_id"] 80 | print(f'Input value for account ID to delegate AWS IC to: {account_id}') 81 | 82 | org_client = boto3.client('organizations') 83 | 84 | # List the delegated administrators for IC 85 | admins = org_client.list_delegated_administrators( 86 | ServicePrincipal='sso.amazonaws.com' 87 | ) 88 | sleep(0.1) 89 | 90 | # Check if any other accounts are delegated admins for IC 91 | other_admins = [admin['Id'] for admin in admins['DelegatedAdministrators'] if admin['Id'] != account_id] 92 | if other_admins: 93 | # Deregister the other delegated admins for IC 94 | for admin_id in other_admins: 95 | print('Deregistreing other delegated administrators for AWS IC') 96 | org_client.deregister_delegated_administrator( 97 | AccountId=admin_id, 98 | ServicePrincipal='sso.amazonaws.com' 99 | ) 100 | print(f'Deregistered {admin_id}') 101 | sleep(0.1) 102 | 103 | # Check if the specified account is already a delegated admin for IC 104 | found = False 105 | for admin in admins['DelegatedAdministrators']: 106 | if admin['Id'] == account_id: 107 | found = True 108 | print(f'{account_id} is already a delegated administrator for AWS IC') 109 | break 110 | 111 | if not found: 112 | # Delegate the specified account as an administrator for IC 113 | org_client.register_delegated_administrator( 114 | AccountId=account_id, 115 | ServicePrincipal='sso.amazonaws.com' 116 | ) 117 | print(f'Delegated {account_id} as administrator for AWS IC') 118 | sleep(0.1) 119 | 120 | 121 | def handler(event, context): 122 | logger.info(event) 123 | logger.debug(context) 124 | request_type = event['RequestType'] 125 | if (request_type == 'Create' or request_type == 'Update'): 126 | try: 127 | delegate_sso_admin(event, context) 128 | except Exception as e: 129 | send(event, context, "FAILED", {'Error': str(e)}) 130 | raise 131 | else: 132 | send(event, context, "SUCCESS", {}) 133 | elif request_type == 'Delete': 134 | send(event, context, "SUCCESS", {}) 135 | else: 136 | send(event, context, "SUCCESS", {}) 137 | 138 | 139 | def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None): 140 | responseUrl = event['ResponseURL'] 141 | 142 | print(responseUrl) 143 | 144 | responseBody = { 145 | 'Status': responseStatus, 146 | 'Reason': reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name), 147 | 'PhysicalResourceId': physicalResourceId or context.log_stream_name, 148 | 'StackId': event['StackId'], 149 | 'RequestId': event['RequestId'], 150 | 'LogicalResourceId': event['LogicalResourceId'], 151 | 'NoEcho': noEcho, 152 | 'Data': responseData 153 | } 154 | 155 | json_responseBody = json.dumps(responseBody) 156 | 157 | print("Response body:") 158 | print(json_responseBody) 159 | 160 | headers = { 161 | 'content-type': '', 162 | 'content-length': str(len(json_responseBody)) 163 | } 164 | 165 | try: 166 | response = http.request( 167 | 'PUT', responseUrl, headers=headers, body=json_responseBody) 168 | print("Status code:", response.status) 169 | 170 | except Exception as e: 171 | 172 | print("send(..) failed executing http.request(..):", e) 173 | Role: 174 | Fn::GetAtt: 175 | - icDelegateAdminLambdaExecutionRole 176 | - Arn 177 | FunctionName: delegate-IC-Admin 178 | Handler: index.handler 179 | MemorySize: 1024 180 | Runtime: python3.12 181 | Timeout: 30 182 | customResourceLambdaInvoke: 183 | Type: Custom::AWS 184 | Properties: 185 | ServiceToken: 186 | Fn::GetAtt: 187 | - icDelegateAdminFunction 188 | - Arn 189 | delegate: !Sub ${delegate} 190 | account_id: !Sub ${accountid} 191 | physicalResourceId: JobSenderTriggerPhysicalId 192 | InstallLatestAwsSdk: true 193 | DependsOn: 194 | - customLambdaInvokePolicy 195 | UpdateReplacePolicy: Delete 196 | DeletionPolicy: Delete 197 | customLambdaInvokePolicy: 198 | Type: AWS::IAM::Policy 199 | Properties: 200 | PolicyDocument: 201 | Statement: 202 | - Action: lambda:InvokeFunction 203 | Effect: Allow 204 | Resource: 205 | Fn::GetAtt: 206 | - icDelegateAdminFunction 207 | - Arn 208 | Version: "2012-10-17" 209 | PolicyName: customLambdaInvokePolicy 210 | Roles: 211 | - !Ref icCustomResourceLambdaExecutionRole 212 | icCustomResourceLambdaExecutionRole: 213 | Type: AWS::IAM::Role 214 | Properties: 215 | AssumeRolePolicyDocument: 216 | Statement: 217 | - Action: sts:AssumeRole 218 | Effect: Allow 219 | Principal: 220 | Service: lambda.amazonaws.com 221 | Version: "2012-10-17" 222 | ManagedPolicyArns: 223 | - Fn::Join: 224 | - "" 225 | - - "arn:" 226 | - !Ref AWS::Partition 227 | - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole -------------------------------------------------------------------------------- /templates/identity-center-s3-bucket.template: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Description: >- 4 | Cloudformation template creating S3 and KMS resources for IAM Identity Center automation 5 | solution. The s3 bucket stores IAM Identity Center permission sets and 6 | assignment mapping files and source code zip files (uksb-8ggfy7rjoj). 7 | Parameters: 8 | ICMappingBucketName: 9 | Type: String 10 | Description: >- 11 | The S3 bucket that stores the source code as well as permission set and 12 | mapping definition. It's the same name that is used in 13 | identity-center-automation.template. 14 | OrganizationId: 15 | Type: String 16 | Description: AWS Organizations ID 17 | createS3KmsKey: 18 | Type: String 19 | AllowedValues: 20 | - "true" 21 | - "false" 22 | Default: "true" 23 | Description: Parameter to check if user wants to create a new KMS key for S3 bucket encryption or use an existing key 24 | S3KmsArn: 25 | Type: String 26 | Description: Existing KMS key ARN for S3 bucket encryption (required if createS3KmsKey is false) 27 | ICAutomationAdminArn: 28 | Type: String 29 | Description: >- 30 | The Name of IAM Identity Center automation admin IAM role. This IAM role 31 | will have permissions to manage S3 bucket, besides ICAutoPipelineCodeBuildRole. 32 | ICKMSAdminArn: 33 | Type: String 34 | Description: >- 35 | The Name of IAM role which will have permissions to manage the IAM Identity Center KMS 36 | key, besides ICAutoPipelineCodeBuildRole. 37 | createICKMSAdminRole: 38 | Type: String 39 | AllowedValues: 40 | - "true" 41 | - "false" 42 | Default: "false" 43 | Description: Parameter to check if user wants to create Identity Center KMS Admin if one does not exist already 44 | createICAdminRole: 45 | Type: String 46 | AllowedValues: 47 | - "true" 48 | - "false" 49 | Default: "false" 50 | Description: Parameter to check if user wants to create Identity Center Admin if one does not exist already 51 | 52 | Conditions: 53 | CreateICKMSAdminRoleEqualsTrue: !Equals [!Ref createICKMSAdminRole, "true"] 54 | CreateICAdminRoleEqualsTrue: !Equals [!Ref createICAdminRole, "true"] 55 | # CreateS3KmsKey: !Equals [ !Ref S3KmsArn, ''] 56 | createKmsKeyEqualsTrue: !Equals [!Ref createS3KmsKey, "true"] 57 | 58 | Resources: 59 | ################################################################################### 60 | # ICAdmin Role to administer Identity Center without triggering the notifications # 61 | ################################################################################### 62 | ICAdminRole: 63 | Type: "AWS::IAM::Role" 64 | Metadata: 65 | cfn_nag: 66 | rules_to_suppress: 67 | - id: F38 68 | reason: "Grant this ICAdminRole IAM full/admin permissions." 69 | - id: F3 70 | reason: "Grant this ICAdminRole SSO full/admin permissions." 71 | Condition: CreateICAdminRoleEqualsTrue 72 | Properties: 73 | AssumeRolePolicyDocument: 74 | Version: "2012-10-17" 75 | Statement: 76 | - Effect: Allow 77 | Principal: 78 | AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" 79 | Action: 80 | - "sts:AssumeRole" 81 | Path: "/" 82 | Policies: 83 | - PolicyName: ICAdminPermissions 84 | PolicyDocument: 85 | Version: "2012-10-17" 86 | Statement: 87 | - Sid: SSOIAMAdminActions 88 | Effect: Allow 89 | Action: 90 | - "sso:*" 91 | - "iam:*" 92 | Resource: "*" 93 | - Sid: S3EssentialObjectActions 94 | Effect: Allow 95 | Action: 96 | - "s3:GetObject" 97 | Resource: !Sub "arn:aws:s3:::${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}/*" 98 | - Sid: S3EssentialBucketAction 99 | Effect: Allow 100 | Action: 101 | - "s3:ListBucket" 102 | Resource: !Sub "arn:aws:s3:::${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}" 103 | ICKMSAdminRole: 104 | Type: "AWS::IAM::Role" 105 | Metadata: 106 | cfn_nag: 107 | rules_to_suppress: 108 | - id: F3 109 | reason: "Grant this ICKMSAdminRole full/admin KMS permissions." 110 | Condition: CreateICKMSAdminRoleEqualsTrue 111 | Properties: 112 | AssumeRolePolicyDocument: 113 | Version: "2012-10-17" 114 | Statement: 115 | - Effect: Allow 116 | Principal: 117 | AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" 118 | Action: 119 | - "sts:AssumeRole" 120 | Path: "/" 121 | Policies: 122 | - PolicyName: ICKMSAdminPermissions 123 | PolicyDocument: 124 | Version: "2012-10-17" 125 | Statement: 126 | - Effect: Allow 127 | Action: 128 | - "kms:*" 129 | Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*" 130 | - Effect: Allow 131 | Action: 132 | - "kms:ListAliases" 133 | Resource: "*" 134 | - Sid: S3EssentialObjectActions 135 | Effect: Allow 136 | Action: 137 | - "s3:GetObject" 138 | Resource: !Sub "arn:aws:s3:::${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}/*" 139 | - Sid: S3EssentialBucketAction 140 | Effect: Allow 141 | Action: 142 | - "s3:ListBucket" 143 | Resource: !Sub "arn:aws:s3:::${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}" 144 | S3Bucket: 145 | Type: AWS::S3::Bucket 146 | DeletionPolicy: Delete 147 | Properties: 148 | BucketName: !Sub "${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}" 149 | VersioningConfiguration: 150 | Status: Enabled 151 | BucketEncryption: 152 | ServerSideEncryptionConfiguration: 153 | - BucketKeyEnabled: true 154 | ServerSideEncryptionByDefault: 155 | SSEAlgorithm: "aws:kms" 156 | KMSMasterKeyID: !If [ createKmsKeyEqualsTrue, !GetAtt S3BucketKMSKey.Arn, !Ref S3KmsArn ] 157 | # PublicAccessBlockConfiguration: 158 | # BlockPublicAcls: True 159 | # BlockPublicPolicy: True 160 | # IgnorePublicAcls: True 161 | # RestrictPublicBuckets: True 162 | 163 | S3BucketPolicy: 164 | Type: AWS::S3::BucketPolicy 165 | Metadata: 166 | cfn_nag: 167 | rules_to_suppress: 168 | - id: F16 169 | reason: "We can allow * for the Principal as we are limiting access to the Org via a Condition." 170 | Properties: 171 | Bucket: !Ref S3Bucket 172 | PolicyDocument: 173 | Version: 2012-10-17 174 | Statement: 175 | - Sid: DenyExternalPrincipals 176 | Effect: Deny 177 | Principal: "*" 178 | Action: "s3:*" 179 | Resource: 180 | - !Sub arn:aws:s3:::${S3Bucket} 181 | - !Sub arn:aws:s3:::${S3Bucket}/* 182 | Condition: 183 | StringNotEquals: 184 | aws:PrincipalOrgID: !Ref OrganizationId 185 | - Sid: SecureTransport 186 | Effect: Deny 187 | Principal: "*" 188 | Action: "s3:*" 189 | Resource: !Sub arn:aws:s3:::${S3Bucket}/* 190 | Condition: 191 | Bool: 192 | "aws:SecureTransport": "false" 193 | - Sid: ProtectBucketDeletion 194 | Action: 195 | - s3:DeleteBucket 196 | Effect: Deny 197 | Resource: !Sub "arn:aws:s3:::${S3Bucket}" 198 | Principal: 199 | AWS: "*" 200 | Condition: 201 | ArnNotLike: 202 | aws:PrincipalArn: 203 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICAutoPipelineCodeBuildRole" 204 | - !If 205 | - CreateICAdminRoleEqualsTrue 206 | - !GetAtt ICAdminRole.Arn 207 | - !Ref ICAutomationAdminArn 208 | - Sid: AllowObjectUpdate 209 | Action: 210 | - s3:DeleteObject 211 | - s3:DeleteObjectVersion 212 | - s3:PutObject 213 | - s3:PutObjectAcl 214 | - s3:PutObjectTagging 215 | Effect: Deny 216 | Principal: "*" 217 | Resource: !Sub arn:aws:s3:::${S3Bucket}/* 218 | Condition: 219 | ArnNotLike: 220 | aws:PrincipalArn: 221 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICAutoPipelineCodeBuildRole" 222 | - !If 223 | - CreateICAdminRoleEqualsTrue 224 | - !GetAtt ICAdminRole.Arn 225 | - !Ref ICAutomationAdminArn 226 | - Sid: OnlyUpdatePolicy 227 | Action: 228 | - s3:PutBucketPolicy 229 | - s3:DeleteBucketPolicy 230 | Effect: Deny 231 | Principal: "*" 232 | Resource: !Sub arn:aws:s3:::${S3Bucket} 233 | Condition: 234 | ArnNotLike: 235 | aws:PrincipalArn: 236 | - !If 237 | - CreateICAdminRoleEqualsTrue 238 | - !GetAtt ICAdminRole.Arn 239 | - !Ref ICAutomationAdminArn 240 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICAutoPipelineCodeBuildRole" 241 | - Sid: require_kms_encryption_on_puts 242 | Action: 243 | - s3:PutObject 244 | Effect: Deny 245 | Principal: "*" 246 | Resource: 247 | - !Sub "arn:aws:s3:::${S3Bucket}/*" 248 | Condition: 249 | StringNotLikeIfExists: 250 | s3:x-amz-server-side-encryption-aws-kms-key-id: 251 | - !If [ createKmsKeyEqualsTrue, !GetAtt S3BucketKMSKey.Arn, !Ref S3KmsArn ] 252 | 253 | S3BucketKMSKey: 254 | Type: "AWS::KMS::Key" 255 | Condition: createKmsKeyEqualsTrue 256 | Metadata: 257 | cfn_nag: 258 | rules_to_suppress: 259 | - id: F76 260 | reason: "Secure CodeBuild Permissions with IAM Condition." 261 | Properties: 262 | Description: Identity Center(IC) mapping bucket Server-side encryption KMS Key 263 | EnableKeyRotation: true 264 | KeyPolicy: 265 | Version: 2012-10-17 266 | Id: keypolicy 267 | Statement: 268 | - Sid: KMS Admin Permissions 269 | Effect: Allow 270 | Principal: 271 | AWS: 272 | - !If 273 | - CreateICKMSAdminRoleEqualsTrue 274 | - !GetAtt ICKMSAdminRole.Arn 275 | - !Ref ICKMSAdminArn 276 | 277 | Action: "kms:*" 278 | Resource: "*" 279 | - Sid: Key management Permissions 280 | Effect: Allow 281 | Principal: 282 | AWS: 283 | - !If 284 | - CreateICAdminRoleEqualsTrue 285 | - !GetAtt ICAdminRole.Arn 286 | - !Ref ICAutomationAdminArn 287 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICAutoPipelineCodeBuildRole" 288 | Action: 289 | - "kms:CancelKeyDeletion" 290 | - "kms:Create*" 291 | - "kms:Delete*" 292 | - "kms:Describe*" 293 | - "kms:Disable*" 294 | - "kms:EnableKeyRotation" 295 | - "kms:GenerateDataKey" 296 | - "kms:Get*" 297 | - "kms:List*" 298 | - "kms:Put*" 299 | - "kms:Revoke*" 300 | - "kms:ScheduleKeyDeletion" 301 | - "kms:TagResource" 302 | - "kms:UntagResource" 303 | Resource: "*" 304 | - Sid: CodeBuild Permissions 305 | Effect: Allow 306 | Principal: 307 | AWS: "*" 308 | Action: 309 | - "kms:Describe*" 310 | - "kms:List*" 311 | - "kms:Get*" 312 | - "kms:Encrypt" 313 | - "kms:Decrypt" 314 | - "kms:ReEncrypt*" 315 | - "kms:GenerateDataKey" 316 | Resource: "*" 317 | Condition: 318 | ForAnyValue:ArnLike: 319 | aws:PrincipalARN: 320 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICAutoPipelineCodeBuildRole" 321 | #IAM Role ARN of IAM Identity Center automation CodeBuild project 322 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICPermissionSetAssignmentAutomationRole" 323 | # - Sid: Allow desribe key permission for other monitor roles 324 | # Effect: Allow 325 | # Principal: 326 | # AWS: 327 | # - !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/access-analyzer.amazonaws.com/AWSServiceRoleForAccessAnalyzer" 328 | # Action: 329 | # - "kms:Describe*" 330 | # - "kms:Get*" 331 | # - "kms:List*" 332 | # Resource: "*" 333 | - Sid: Allow CodeBuild to use objects from S3 334 | Effect: Allow 335 | Principal: 336 | Service: codebuild.amazonaws.com 337 | Action: 338 | - "kms:Decrypt" 339 | - "kms:DescribeKey" 340 | - "kms:Encrypt" 341 | - "kms:GenerateDataKey" 342 | - "kms:GenerateDataKeyWithoutPlaintext" 343 | - "kms:ReEncrypt*" 344 | Resource: "*" 345 | Condition: 346 | StringEquals: 347 | "kms:ViaService": !Sub "codebuild.${AWS::Region}.amazonaws.com" 348 | 349 | Outputs: 350 | ICKMSAdminRole: 351 | Value: 352 | Fn::If: 353 | - CreateICKMSAdminRoleEqualsTrue 354 | - !GetAtt ICKMSAdminRole.Arn 355 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/${ICKMSAdminArn}" 356 | Description: The ARN of IAM Identity Center automation admin IAM role or IAM user. This IAM role(or user) will have permissions to update IAM Identity Center settings without trigger the SNS notification, besides the ICPermissionSetAssignmentAutomationRole. 357 | Export: 358 | Name: ICKMSAdminRoleArn 359 | ICAdminRole: 360 | Value: 361 | Fn::If: 362 | - CreateICAdminRoleEqualsTrue 363 | - !GetAtt ICAdminRole.Arn 364 | - !Ref ICAutomationAdminArn 365 | Description: The ARN of IAM Identity Center automation admin IAM role or IAM user. This IAM role(or user) will have permissions to update IAM Identity Center settings without trigger the SNS notification, besides the ICPermissionSetAssignmentAutomationRole. 366 | Export: 367 | Name: ICAdminRoleArn 368 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.0.0 4 | 5 | - initial push. 6 | 7 | ## 1.1.0 8 | 9 | - Updated auto-permissionsets.py file to support customer managed policy in permission set. 10 | - Updated the permission set example 5-example-sec-readonly.json. 11 | - Updated auto-permissionsets.py and identity-center-automation.template to support custom permission set session duration. 12 | - Default session duration is set to 1 hour. 13 | - Updated the permission set example 1-example-admin.json. 14 | 15 | ## 2.0.0 16 | 17 | - Updated identity-center-stacks-parameters.json to get additional parameters from users to support delegated administration for Identity Center and AWS Control Tower enabled accounts. 18 | - Check if admin is delegated for IAM Identity Center. 19 | - Check if AWS Control Tower is enabled. 20 | - Check if administrative IAM user or role for Identity Center exists in account or to deploy a new IAM role. 21 | - Check if administrative IAM user or role for KMS exists in account or to deploy a new IAM role. 22 | - Updated codepipeline-stack.template to support delegated administration for Identity Center and AWS Control Tower enabled accounts. 23 | - Updated S3 bucket name to make it a unique identifier. 24 | - Added permissions to CodeBuild IAM role to create, update and delete DynamoDB table. 25 | - Updated CodePipeline pipeline name from Identity-Center-Automation-Sample-Solution to Identity-Center-Automation-Solution. 26 | - Added IC-Delegate-Admin.yml to allow delegating administration for IAM Identity Center to a Organization member account. 27 | - Updated architecture_diagram.png to reflect new features in the architecture diagram. 28 | - Updated identity-center-automation.template to support delegated administration for Identity Center and AWS Control Tower enabled accounts. 29 | - Added additional parameters to support management account and Control Tower provisioned permission sets and assignments. 30 | - Added DynamoDB table to store skipped permission set names and ARNs, if necessary. 31 | - Added additional environment variables to Lambda functions auto-permissionsets and auto-assignment. 32 | - Added permissions to add, remove and batch update skipped permission set names and ARNs to the DynamoDB table 33 | - Added permissions to Lambda execution IAM role to get the list of accounts for a provisioned Permission Set to support delegated admin and Control Tower enabled feature. 34 | - Updated identity-center-s3-bucket.template to support delegated administration for Identity Center and AWS Control Tower enabled accounts. 35 | - Added additional parameters and corresponding conditions to check if Identity Center and KMS administrator role or user exists in the account, if not, added an option to create as a part of the stack. 36 | - Updated S3 bucket and KMS key policy to include the appropriate admin IAM roles as principals. 37 | - Exported outputs to be imported by the identity-center-automation.template if new IAM roles are created. 38 | - Updated buildspec-param.yml to support delegated administration for Identity Center and AWS Control Tower enabled accounts. 39 | - Updated commands to pass the additional parameters to the corresponding CloudFormation stacks. 40 | - Updated auto-assignment.py to support delegated administration for Identity Center and AWS Control Tower enabled accounts. 41 | - Added code to skip Permission Sets provisioned in management account if Control Tower is enabled or if a Organization member account is delegated as an administrator for Identity Center. 42 | - Added code to skip permission sets tagged with tag key ManagedBy and value ControlTower in order to prevent the automation from removing Permission Sets created by Control Tower. 43 | - Updated auto-permissionsets.py to support delegated administration for Identity Center and AWS Control Tower enabled accounts. 44 | - Added code to skip Permission Sets provisioned in management account if Control Tower is enabled or if a Organization member account is delegated as an administrator for Identity Center. 45 | - Added code to add skipped permission set names and ARNs the DynamoDB table visibility and auditing. 46 | - Added code to skip permission sets tagged with tag key ManagedBy and value ControlTower in order to prevent the automation from removing Permission Sets created by Control Tower and add permission set names and ARNs to IC-SkippedPermissionSets for visibility and auditing. 47 | - Updated code to sync skipped permission sets with DynamoDB table to ensure items in the table are not in drift 48 | - Updated code to handle existing Permission Sets without description. 49 | - Updated code to remove drift when triggered by EventBridge rule on detecting manual changes to Identity Center 50 | 51 | ## 2.0.1 52 | 53 | - Updated the IAM, KMS, and S3 permissions in the codepipeline-stack.template, identity-center-automation.template, and identity-center-s3-bucket.template to fix cfn_scan failures. 54 | - Updated buildspec-param.yml to add CFN linting and secure checks using [cfn_nag_scan](https://github.com/stelligent/cfn_nag). 55 | - The CodeBuild task will fail if cfn_nag_scan detects any failure in the CloudFormation templates. 56 | 57 | ## 2.1.0 58 | 59 | - Bug fix: Pipeline failed with 'Parameter validation failed: Missing required parameter in input: "InstanceArn"' in cases where more than the response returned more than 100 items and needed to be iterated through. 60 | - Added the missing InstanceArn=ic_instance_arn to allow proper functioning of list_accounts_for_provisioned_permission_set API. 61 | - Bug fix: any updates to the Lambda source code did not update the Lambda function code upon successful pipeline execution. 62 | - Updated the buildspec to add tags to the lambda zip file objects in S3 and obtain object versions. The versions are now referenced in the Lambda cofiguration to allow subsequent updates to lambda package code. 63 | - Updated the list_groups API to get_group_id API and removed the use of depricated filter method to obtain group Id by name in the auto-assignment.py. 64 | - list_groups returns a paginated list of complete Group objects. Filtering for a Group by the DisplayName attribute is deprecated. Instead, GetGroupId API action will be used to obtain group Id by name. 65 | - Updated the Lambda runtime to the latest python3.12. 66 | - Updated SNS subscription protocol to email in the identity-center-automation.template 67 | - Email as compared to email-json allowed adding formatting to the message within ic-alert-SNSnotification lambda function to send formatted and prettier JSON message. This improves readability of the Identity Center manual modification alerts. 68 | - Updated the Identity Center automation pipeline in the codepipeline-stack.template to event-driven pipeline. 69 | - It is recommended to use event-based change detection for pipelines as opposed to polling for changes. 70 | - Updated pipeline stage names to better reflect their purpose. 71 | 72 | ## 3.0.0 73 | 74 | ### Major Architectural Changes 75 | 76 | - Replaced Lambda functions with CodeBuild projects for core automation: 77 | - Migrated auto-permissionsets.py and auto-assignment.py from Lambda to CodeBuild. 78 | - This change allows for longer execution times (no timeout issues), more memory, and easier dependency management. 79 | - Implemented CodePipeline V2: 80 | - Updated codepipeline-stack.template to use the latest CodePipeline features. 81 | - Added support for CodeStar connections: 82 | - Allows integration with various source control providers beyond AWS CodeCommit. 83 | - Updated codepipeline-stack.template to include CodeStar connection options. 84 | - Enhanced S3 bucket management: 85 | - Implemented versioning for S3 objects to ensure data integrity and allow rollbacks. 86 | - Added option to create a new KMS key for S3 bucket encryption or use an existing key. 87 | 88 | ### New Features 89 | 90 | - Syntax validation for permission sets and mapping files: 91 | - Added syntax-validator.py to perform comprehensive checks on JSON structures. 92 | - Validates permission set names, ARNs, policy structures, and mapping file format and content. 93 | - Generation of permission sets and mapping files: 94 | - New feature to generate JSON files from existing Identity Center configuration. 95 | - Added auto-generate-permissionsets-mapping-files.py to facilitate this process. 96 | - Added EventBridge rule to automatically trigger CodeBuild project when auto-generate-permissionsets-mapping-files.py is uploaded to S3, for JSON identity-center-mapping-info files generation. 97 | - Support for account names and OU names in target mappings: 98 | - Enhanced auto-assignment.py to resolve account names and OUs, including nested OUs, in addition to using just account IDs. 99 | - Allows more human-readable and flexible target specifications in mapping files. 100 | - Support for permission boundaries: 101 | - Added ability to define and manage permission boundaries for permission sets, using both AWS Managed and Customer Managed Policies. 102 | - Scheduled baselining of Identity Center configuration: 103 | - Added EventBridge rule to periodically (Every 12 hours) trigger the automation process. 104 | - Helps maintain desired state even if manual changes are made outside the pipeline. 105 | - Improved event-driven triggers: 106 | - Updated EventBridge rules to trigger on successful account creation and invitation acceptance. 107 | - More precise and efficient handling of organizational changes. 108 | - Moved CloudFormation templates to a dedicated templates directory for better organization. 109 | 110 | ### Enhancements 111 | 112 | - Error handling and logging improvements: 113 | - Implemented more granular error catching and reporting across all scripts. 114 | - Added detailed logging for better traceability and debugging. 115 | - Enhanced logging with CloudWatch integration into CodeBuild project for better traceability. 116 | - Optimized API calls: 117 | - Implemented batching and pagination for certain AWS API calls to reduce the risk of throttling. 118 | - Handling of suspended accounts and accounts pending closure: 119 | - Updated account processing logic to skip accounts in these states. 120 | - Prevents unnecessary operations on inactive accounts. 121 | - Improved account assignment process: 122 | - Enhanced logic for detecting and handling changes in account assignments. 123 | - Implemented more efficient provisioning and deprovisioning of permission sets. 124 | - Expanded Organizations integration: 125 | - Added support for retrieving and working with nested organizational units. 126 | - IAM permission refinements: 127 | - Implemented least privilege principle more strictly across all IAM roles. 128 | - Updated IAM policies in codepipeline-stack.template, identity-center-automation.template, and identity-center-s3-bucket.template. 129 | - Updated IAM roles with more granular permissions for CodeBuild, EventBridge, and other AWS services. 130 | - Dependency updates: 131 | - Upgraded boto3 and other Python dependencies to latest compatible versions. 132 | - Improved handling of Control Tower managed permission sets: 133 | - Enhanced logic to identify and preserve Control Tower managed resources. 134 | - Updated skipping mechanism for these permission sets to prevent unintended modifications. 135 | - Performance optimizations: 136 | - Implemented batching for certain AWS API calls to reduce the risk of throttling. 137 | - Optimized loops and data structures for better efficiency in large-scale environments. 138 | - Compatibility checks: 139 | - Added checks to ensure backward compatibility with existing deployment structures where possible. 140 | - Provided migration guidance for users upgrading from previous versions. 141 | 142 | ### Bug Fixes 143 | 144 | - Fixed issues with whitespace handling in permission set and group names. 145 | - Corrected validation logic for various fields in permission sets and mapping files. 146 | - Addressed potential race conditions in account assignment operations. 147 | - Fixed the timeout issue in automation. 148 | 149 | ### File and Template Updates 150 | 151 | - codepipeline-stack.template: 152 | - Added parameters for CodeStar connections and source control options. 153 | - Updated IAM roles and policies for CodeBuild projects with more granular access. 154 | - Implemented new stages for syntax validation. 155 | - Added a standalone CodeBuild Project for JSON files generation. 156 | - Added EventBridge configuration for S3 bucket. 157 | - Created new EventBridge rule to trigger CodeBuild project for auto-generation of mapping files. 158 | - Added new IAM role for EventBridge to start CodeBuild projects. 159 | - identity-center-automation.template: 160 | - Removed Lambda resources and added CodeBuild project configurations. 161 | - Updated EventBridge rules to trigger CodeBuild projects instead of Lambda functions. 162 | - Added new parameters for CodeBuild project names and artifact bucket. 163 | - Updated IAM permissions to include new SSO and Organizations actions. 164 | - Added support for new SSO API calls related to permission boundaries and account assignments. 165 | - identity-center-s3-bucket.template: 166 | - Added options for KMS key creation and management. 167 | - Updated bucket policies to reflect new versioning requirements. 168 | - buildspec files: 169 | - Created new buildspec files for syntax validation, permission set creation, and assignment processes. 170 | - Updated existing buildspecs to align with new CodeBuild project structure. 171 | - Python scripts: 172 | - Significant updates to auto-permissionsets.py and auto-assignment.py to work in CodeBuild environment. 173 | - Updated logging statements in all python scrips for better visibility. 174 | - Added new validation functions and improved error handling. 175 | - Implemented logic to handle account names and OU names in target mappings. 176 | 177 | ### Documentation Updates 178 | 179 | - Updated README.md: 180 | - Revised architecture diagram to reflect new components and workflows. 181 | - Updated implementation instructions for both management account and delegated administrator scenarios. 182 | - Added sections explaining new features like syntax validation and file generation. 183 | - Added more detailed implementation instructions and examples. 184 | - Added new sections explaining new features like auto-generation of files and OU-based assignments. 185 | - Improved explanation of permission set, global, and target mapping structure and supported fields, with examples. 186 | - Updated example JSON files: 187 | - Revised permission set and mapping file examples to showcase new capabilities. 188 | - Added examples demonstrating the use of account names and OU names in target mappings, as wel as permission boundaries in permission set files. 189 | 190 | ## 3.1.0 191 | 192 | - Performance Improvements 193 | - Added ThreadPoolExecutor with configurable worker pools for parallel processing 194 | - ASSIGNMENT_WORKERS = 10 (For CreateAccountAssignment) 195 | - GENERAL_WORKERS = 20 (For read/list operations and other create operations) 196 | - Implemented CacheManager class with TTL for API call results 197 | - Optimized AWS API connection pooling configuration 198 | - Added batching for assignment operations to stay within API limits 199 | - Implemented parallel processing for permission set and assignment operations including: 200 | - Permission set provisioning, deprovisioining and deletion 201 | - Account assignments 202 | - Permission set components and attributes synchronization 203 | - Policy management 204 | 205 | - Optimizations 206 | - Reduced redundant API calls through caching 207 | - Improved account status checking with caching 208 | - Optimized permission set provisioning logic 209 | - Enhanced deprovisioning workflow efficiency 210 | - Improved tag synchronization using set operations 211 | - Implemented batch processing for API operations 212 | 213 | - Reliability Improvements 214 | - Added exponential backoff retry logic for API calls 215 | - Base delay: 1 second 216 | - Max attempts: 5 217 | - Adaptive retry mode 218 | - Enhanced error handling with granular exception catching 219 | - Implemented robust status tracking for provisioning operations 220 | - Added progress tracking for long-running operations 221 | - Improved handling of throttling and conflict scenarios 222 | 223 | - Code Quality 224 | - Refactored code to use set operations for efficient comparisons 225 | - Added type hints and improved documentation 226 | - Implemented consistent logging patterns 227 | - Added cache statistics for monitoring and debugging 228 | - Reorganized code structure for better maintainability 229 | 230 | - Bug Fixes 231 | - Fixed potential race conditions in assignment operations 232 | - Corrected handling of empty inline policies 233 | - Improved error handling in provisioning status checks 234 | - Fixed issue with concurrent provisioning requests that led to API throttling 235 | - Enhanced handling of API throttling 236 | 237 | - Testing Notes 238 | The changes have been tested in both management account and delegated administrator scenarios. Performance improvements show significant reduction in execution time for large-scale environments. 239 | 240 | - Breaking Changes 241 | None. This is a non-breaking change focused on internal optimizations. 242 | 243 | - Compatibility 244 | Compatible with existing deployment structures and configuration files (pre-v3.0.0) and the new configuration file structure (v3.0.0 onwards). 245 | 246 | ## 3.1.1 247 | 248 | ### Bug Fixes & Improvements 249 | 250 | - Fixed global mapping target check to include 'TargetAccountid' field fallback 251 | - Updated Sid validation in inline policy statements: 252 | - Added type checking for Sid values 253 | - Restricted Sid pattern to alphanumeric characters only [a-z, A-Z, 0-9] 254 | - Added proper pagination for outdated permission sets check 255 | - Fixed log message formatting for managed policy operations 256 | - Removed deprecated start_automation function 257 | - Enhanced empty permission set component handling using .get() with default values 258 | 259 | ### Notes 260 | 261 | - Non-breaking changes focused on validation and bug fixes 262 | - Compatible with existing deployment structures 263 | 264 | ## 3.1.2 265 | 266 | ### Bug Fixes & Improvements 267 | 268 | - **IAM Role ARN Validation**: Improved syntax validator to support AWS Identity Center (SSO) reserved role paths (`aws-reserved/sso.amazonaws.com/`) 269 | - **Service-Linked Role Support**: Enhanced validation for AWS service-linked roles with proper path handling 270 | - **Role Name Length Validation**: Refined role name length validation to support nested role paths 271 | - **CloudFormation Templates**: Updated all template descriptions with current solution identifier (`uksb-8ggfy7rjoj`) 272 | 273 | ### Notes 274 | 275 | - Non-breaking changes focused on validation and bug fixes 276 | - Compatible with existing deployment structures 277 | -------------------------------------------------------------------------------- /src/automation-code/permission-set-and-mapping-files-generator/auto-generate-permissionsets-mapping-files.py: -------------------------------------------------------------------------------- 1 | """Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved""" 2 | """ 3 | This script fetches existing permission sets and assignments from AWS IAM Identity Center 4 | and creates the corresponding JSON files in the identity-center-mapping-info directory structure. 5 | """ 6 | 7 | 8 | from typing import Dict, List, Set, Tuple 9 | from botocore.config import Config 10 | import os 11 | import json 12 | import boto3 13 | import logging 14 | from time import sleep 15 | from botocore.exceptions import ClientError 16 | logger = logging.getLogger() 17 | logger.setLevel(logging.INFO) 18 | 19 | # Stream handler to print logs on screen 20 | console_handler = logging.StreamHandler() 21 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 22 | console_handler.setFormatter(formatter) 23 | logger.addHandler(console_handler) 24 | 25 | boto_logger = logging.getLogger('botocore') 26 | boto_logger.setLevel(logging.DEBUG) 27 | 28 | 29 | for handler in logger.handlers: 30 | boto_logger.addHandler(handler) 31 | 32 | 33 | class RetryFilter(logging.Filter): 34 | def filter(self, record): 35 | return 'Retry needed' in record.getMessage() 36 | 37 | 38 | boto_logger.addFilter(RetryFilter()) 39 | 40 | AWS_CONFIG = Config( 41 | retries=dict( 42 | max_attempts=100, 43 | mode='adaptive' 44 | ) 45 | ) 46 | 47 | runtime_region = os.getenv('AWS_REGION') 48 | ic_admin = boto3.client( 49 | 'sso-admin', region_name=runtime_region, config=AWS_CONFIG) 50 | identitystore_client = boto3.client( 51 | 'identitystore', region_name=runtime_region, config=AWS_CONFIG) 52 | sts_client = boto3.client( 53 | 'sts', region_name=runtime_region, config=AWS_CONFIG) 54 | organizations = boto3.client('organizations', config=AWS_CONFIG) 55 | ic_instance_arn = os.getenv('IC_INSTANCE_ARN') 56 | identity_store_id = os.getenv('IDENTITY_STORE_ID') 57 | 58 | 59 | # Global variables to store delegated admin status and management account ID 60 | IS_DELEGATED = None 61 | MANAGEMENT_ACCOUNT_ID = None 62 | skip_management_perm_sets = set() 63 | 64 | 65 | def initialize_global_vars(): 66 | """Initialize global variables for delegated admin status and management account ID""" 67 | global IS_DELEGATED, MANAGEMENT_ACCOUNT_ID, skip_management_perm_sets 68 | skip_management_perm_sets = set() 69 | try: 70 | org_response = organizations.describe_organization() 71 | MANAGEMENT_ACCOUNT_ID = org_response['Organization']['MasterAccountId'] 72 | IS_DELEGATED = sts_client.get_caller_identity( 73 | )['Account'] != MANAGEMENT_ACCOUNT_ID 74 | if IS_DELEGATED: 75 | logger.info("Running in delegated admin account") 76 | except Exception as e: 77 | logger.error(f"Error initializing global variables: {str(e)}") 78 | IS_DELEGATED = False 79 | MANAGEMENT_ACCOUNT_ID = None 80 | 81 | 82 | console_handler.setLevel(logging.INFO) 83 | 84 | 85 | def get_permission_set_details(perm_set_arn): 86 | """Get detailed information about a permission set including policies and tags""" 87 | logger.info(f"Getting details for permission set: {perm_set_arn}") 88 | try: 89 | describe_perm_set = ic_admin.describe_permission_set( 90 | InstanceArn=ic_instance_arn, 91 | PermissionSetArn=perm_set_arn 92 | ) 93 | perm_set = describe_perm_set['PermissionSet'] 94 | sleep(0.1) 95 | 96 | managed_policies = [] 97 | 98 | paginator = ic_admin.get_paginator( 99 | 'list_managed_policies_in_permission_set') 100 | for page in paginator.paginate( 101 | InstanceArn=ic_instance_arn, 102 | PermissionSetArn=perm_set_arn 103 | ): 104 | for policy in page.get('AttachedManagedPolicies', []): 105 | managed_policies.append({ 106 | "Name": policy['Name'], 107 | "Arn": policy['Arn'] 108 | }) 109 | sleep(0.1) 110 | 111 | # Get customer managed policies if they exist 112 | customer_managed_policies = [] 113 | try: 114 | paginator = ic_admin.get_paginator( 115 | 'list_customer_managed_policy_references_in_permission_set') 116 | for page in paginator.paginate( 117 | InstanceArn=ic_instance_arn, 118 | PermissionSetArn=perm_set_arn 119 | ): 120 | for policy in page.get('CustomerManagedPolicyReferences', []): 121 | customer_managed_policies.append({ 122 | "Name": policy['Name'], 123 | "Path": policy.get('Path', '/') 124 | }) 125 | sleep(0.1) 126 | except ic_admin.exceptions.ResourceNotFoundException: 127 | pass 128 | 129 | # Get permissions boundary if it exists 130 | try: 131 | boundary = ic_admin.get_permissions_boundary_for_permission_set( 132 | InstanceArn=ic_instance_arn, 133 | PermissionSetArn=perm_set_arn 134 | ).get('PermissionsBoundary', None) 135 | if boundary: 136 | boundary_details = {} 137 | if 'ManagedPolicyArn' in boundary: 138 | boundary_details['Arn'] = boundary['ManagedPolicyArn'] 139 | if '/' in boundary['ManagedPolicyArn']: 140 | boundary_details['Name'] = boundary['ManagedPolicyArn'].split( 141 | '/')[-1] 142 | if 'CustomerManagedPolicyReference' in boundary: 143 | boundary_details['Name'] = boundary['CustomerManagedPolicyReference']['Name'] 144 | boundary_details['Path'] = boundary['CustomerManagedPolicyReference'].get( 145 | 'Path', '/') 146 | else: 147 | boundary_details = None 148 | sleep(0.1) 149 | except ic_admin.exceptions.ResourceNotFoundException: 150 | boundary_details = None 151 | 152 | # Get inline policy if it exists 153 | try: 154 | inline_policy = ic_admin.get_inline_policy_for_permission_set( 155 | InstanceArn=ic_instance_arn, 156 | PermissionSetArn=perm_set_arn 157 | ).get('InlinePolicy', None) 158 | sleep(0.1) 159 | except ic_admin.exceptions.ResourceNotFoundException: 160 | inline_policy = None 161 | 162 | # Get tags 163 | tags_response = ic_admin.list_tags_for_resource( 164 | InstanceArn=ic_instance_arn, 165 | ResourceArn=perm_set_arn 166 | ) 167 | 168 | all_tags = tags_response.get('Tags', []) 169 | 170 | while 'NextToken' in tags_response: 171 | tags_response = ic_admin.list_tags_for_resource( 172 | InstanceArn=ic_instance_arn, 173 | NextToken=tags_response['NextToken'], 174 | ResourceArn=perm_set_arn 175 | ) 176 | all_tags.extend(tags_response.get('Tags', [])) 177 | 178 | sleep(0.1) 179 | 180 | # Create permission set JSON 181 | perm_set_json = { 182 | "Name": perm_set['Name'], 183 | "Description": perm_set.get('Description', perm_set['Name']), 184 | "Session_Duration": perm_set.get('SessionDuration', 'PT1H'), 185 | "Tags": all_tags, 186 | "ManagedPolicies": managed_policies, 187 | "CustomerPolicies": customer_managed_policies, 188 | "InlinePolicies": json.loads(inline_policy) if inline_policy else [] 189 | } 190 | 191 | if boundary_details: 192 | perm_set_json["PermissionsBoundary"] = boundary_details 193 | 194 | return perm_set_json 195 | 196 | except Exception as e: 197 | logger.error( 198 | f"Error getting permission set details for {perm_set_arn}: {str(e)}") 199 | raise 200 | 201 | 202 | def get_group_name(group_id, identity_store_id, group_display_names): 203 | """Get group display name from cache or Identity Store""" 204 | if group_id in group_display_names: 205 | return group_display_names[group_id] 206 | 207 | try: 208 | # Get group details from Identity Store 209 | group_response = identitystore_client.describe_group( 210 | IdentityStoreId=identity_store_id, 211 | GroupId=group_id 212 | ) 213 | sleep(0.1) 214 | # Get the display name, Group ID if not found 215 | display_name = group_response.get('DisplayName') 216 | if not display_name: 217 | logger.warning(f"No display name found for group ID {group_id}") 218 | return group_id 219 | 220 | # Store both mappings to handle lookups by either ID or name 221 | group_display_names[group_id] = display_name 222 | group_display_names[display_name] = display_name 223 | 224 | return display_name 225 | except Exception as e: 226 | logger.warning(f"Error getting group name for ID {group_id}: {str(e)}") 227 | return group_id 228 | 229 | 230 | def get_account_assignments(): 231 | """Get all account assignments and organize them into global and target mappings with optimized processing""" 232 | # Initialize required variables 233 | account_name_cache = {} 234 | global_mapping = [] 235 | target_mapping = [] 236 | group_display_names = {} 237 | accounts = [] 238 | active_account_ids = set() 239 | global skip_management_perm_sets # Use global variables 240 | 241 | def get_account_name(acc_id): 242 | account_name = next((acc['Name'] 243 | for acc in accounts if acc['Id'] == acc_id), None) 244 | if account_name: 245 | return account_name 246 | 247 | try: 248 | acc = organizations.describe_account(AccountId=acc_id)['Account'] 249 | return acc['Name'] 250 | except Exception: 251 | return None 252 | 253 | try: 254 | mgmt_acc = organizations.describe_account( 255 | AccountId=MANAGEMENT_ACCOUNT_ID)['Account'] 256 | accounts.append(mgmt_acc) 257 | account_name_cache[mgmt_acc['Id']] = mgmt_acc['Name'] 258 | if mgmt_acc['Status'] != 'SUSPENDED': 259 | active_account_ids.add(mgmt_acc['Id']) 260 | 261 | accounts_paginator = organizations.get_paginator('list_accounts') 262 | for page in accounts_paginator.paginate(): 263 | sleep(0.1) 264 | for account in page['Accounts']: 265 | if account['Id'] == MANAGEMENT_ACCOUNT_ID: 266 | continue 267 | accounts.append(account) 268 | account_name_cache[account['Id']] = account['Name'] 269 | if account['Status'] != 'SUSPENDED': 270 | active_account_ids.add(account['Id']) 271 | 272 | perm_set_cache = {} 273 | all_perm_sets = [] 274 | perm_sets_response = ic_admin.list_permission_sets( 275 | InstanceArn=ic_instance_arn, 276 | MaxResults=100 277 | ) 278 | all_perm_sets.extend(perm_sets_response['PermissionSets']) 279 | 280 | while 'NextToken' in perm_sets_response: 281 | perm_sets_response = ic_admin.list_permission_sets( 282 | InstanceArn=ic_instance_arn, 283 | NextToken=perm_sets_response['NextToken'], 284 | MaxResults=100 285 | ) 286 | all_perm_sets.extend(perm_sets_response['PermissionSets']) 287 | 288 | for perm_set_arn in all_perm_sets: 289 | try: 290 | response = ic_admin.describe_permission_set( 291 | InstanceArn=ic_instance_arn, 292 | PermissionSetArn=perm_set_arn 293 | ) 294 | sleep(0.1) 295 | perm_set_cache[perm_set_arn] = response['PermissionSet']['Name'] 296 | except Exception as e: 297 | logger.error( 298 | f"Error getting permission set details for {perm_set_arn}: {str(e)}") 299 | 300 | assignments_by_group = {} 301 | for perm_set_arn in all_perm_sets: 302 | perm_set_name = perm_set_cache.get(perm_set_arn) 303 | if not perm_set_name: 304 | continue 305 | 306 | logger.info(f"Processing permission set: {perm_set_name}") 307 | 308 | # Process all active accounts for this permission set 309 | for account_id in active_account_ids: 310 | try: 311 | assignments_response = ic_admin.list_account_assignments( 312 | InstanceArn=ic_instance_arn, 313 | AccountId=account_id, 314 | PermissionSetArn=perm_set_arn, 315 | MaxResults=100 316 | ) 317 | sleep(0.1) 318 | 319 | assignments = assignments_response.get( 320 | 'AccountAssignments', []) 321 | while 'NextToken' in assignments_response: 322 | assignments_response = ic_admin.list_account_assignments( 323 | InstanceArn=ic_instance_arn, 324 | AccountId=account_id, 325 | PermissionSetArn=perm_set_arn, 326 | MaxResults=100, 327 | NextToken=assignments_response['NextToken'] 328 | ) 329 | assignments.extend(assignments_response.get( 330 | 'AccountAssignments', [])) 331 | 332 | # If this is the management account and we're in delegated mode, 333 | # mark this permission set to be skipped if it has any assignments 334 | if IS_DELEGATED and account_id == MANAGEMENT_ACCOUNT_ID and assignments: 335 | skip_management_perm_sets.add(perm_set_arn) 336 | logger.info( 337 | f"Marking permission set {perm_set_name} to be skipped (provisioned in management account)") 338 | continue 339 | 340 | for assignment in assignments: 341 | if assignment['PrincipalType'] != 'GROUP': 342 | continue 343 | 344 | group_id = assignment['PrincipalId'] 345 | group_name = get_group_name( 346 | group_id, identity_store_id, group_display_names) 347 | 348 | if not group_name: 349 | logger.warning( 350 | f"Warning: Empty group name encountered") 351 | continue 352 | 353 | if group_name not in assignments_by_group: 354 | assignments_by_group[group_name] = {} 355 | 356 | # Skip if this permission set is provisioned in management account 357 | if IS_DELEGATED and perm_set_arn in skip_management_perm_sets: 358 | continue 359 | 360 | if perm_set_name not in assignments_by_group[group_name]: 361 | assignments_by_group[group_name][perm_set_name] = set( 362 | ) 363 | 364 | assignments_by_group[group_name][perm_set_name].add( 365 | account_id) 366 | logger.debug( 367 | f"Successfully added account {account_id} to {group_name}/{perm_set_name}") 368 | 369 | except Exception as e: 370 | logger.error( 371 | f"Error processing assignments for account {account_id}: {str(e)}") 372 | 373 | # Identify global vs target assignments 374 | seen_groups = set() 375 | 376 | for group_name, perm_sets_data in assignments_by_group.items(): 377 | if group_name in seen_groups: 378 | continue 379 | 380 | display_name = group_display_names.get(group_name, group_name) 381 | seen_groups.add(group_name) 382 | seen_groups.add(display_name) 383 | 384 | logger.info(f"Processing assignments for group: {display_name}") 385 | global_perm_sets = [] 386 | target_perm_sets = {} 387 | 388 | for curr_perm_set_name, assigned_accounts in perm_sets_data.items(): 389 | # Skip if this permission set is in the skip list (management account permission sets) 390 | curr_perm_set_arn = next((arn for arn in all_perm_sets if perm_set_cache.get( 391 | arn) == curr_perm_set_name), None) 392 | if curr_perm_set_arn in skip_management_perm_sets: 393 | logger.info( 394 | f"Skipping {curr_perm_set_name} in target mapping as it is provisioned in management account") 395 | continue 396 | 397 | assigned_accounts_list = sorted(list(assigned_accounts)) 398 | covered_accounts = set() 399 | non_management_active_accounts = [] 400 | 401 | # Process accounts and organize them into the target structure 402 | logger.debug( 403 | f"Processing assignments for {display_name}, perm set: {curr_perm_set_name}") 404 | target_list = [] 405 | account_names = set() 406 | 407 | # Process all accounts directly 408 | for account_id in assigned_accounts_list: 409 | account_name = get_account_name(account_id) 410 | if account_name: 411 | account_names.add(account_name) 412 | covered_accounts.add(account_id) 413 | 414 | # Add all accounts to the target list 415 | if account_names: 416 | target_list.append({ 417 | "Accounts": sorted(list(account_names)) 418 | }) 419 | 420 | logger.info( 421 | f"Collecting accounts for {display_name}: {curr_perm_set_name}") 422 | covered_accounts = set() # Reset for this permission set 423 | try: 424 | if MANAGEMENT_ACCOUNT_ID in active_account_ids: 425 | covered_accounts.add(MANAGEMENT_ACCOUNT_ID) 426 | mgmt_acc = organizations.describe_account( 427 | AccountId=MANAGEMENT_ACCOUNT_ID)['Account'] 428 | account_names.add(mgmt_acc['Name']) 429 | logger.debug( 430 | f"Added management account {MANAGEMENT_ACCOUNT_ID} to covered accounts") 431 | except Exception as e: 432 | logger.error(f"Error getting management account: {str(e)}") 433 | 434 | if IS_DELEGATED: 435 | logger.info( 436 | f"Delegated admin detected. \ 437 | Permission set assigned with all accounts except management account will be considered global as management account permission sets MUST be created and provisioned from the management account. \ 438 | Checking if {curr_perm_set_name} is global...") 439 | # add all accounts to global if running in delegated admin and assigned to all accounts or all accounts except management account. 440 | # all_accounts_assigned = all( 441 | # acc_id in assigned_accounts for acc_id in active_account_ids) 442 | 443 | non_management_active_accounts = [ 444 | acc for acc in active_account_ids if acc != MANAGEMENT_ACCOUNT_ID] 445 | all_non_management_assigned = sorted( 446 | assigned_accounts_list) == sorted(non_management_active_accounts) 447 | 448 | if all_non_management_assigned: 449 | global_perm_sets.append(curr_perm_set_name) 450 | elif target_list: 451 | target_perm_sets[curr_perm_set_name] = target_list 452 | else: 453 | logger.info( 454 | f"Delegated admin not detected. Identity Center running in Management account \ 455 | Permission set assigned with all accounts including the management account will be considered global. \ 456 | Checking if {curr_perm_set_name} is global...") 457 | # For non-delegated admin, only add to global if assigned to all accounts 458 | if all(acc_id in assigned_accounts for acc_id in active_account_ids): 459 | global_perm_sets.append(curr_perm_set_name) 460 | elif target_list: 461 | target_perm_sets[curr_perm_set_name] = target_list 462 | 463 | # Add mapping data 464 | if global_perm_sets: 465 | global_mapping.append({ 466 | "GlobalGroupName": display_name, 467 | "PermissionSetName": sorted(global_perm_sets), 468 | "Target": "Global" 469 | }) 470 | 471 | if target_perm_sets: 472 | for perm_set_name, targets in target_perm_sets.items(): 473 | target_mapping.append({ 474 | "TargetGroupName": display_name, 475 | "PermissionSetName": [perm_set_name], 476 | "Target": targets 477 | }) 478 | 479 | return global_mapping, target_mapping 480 | 481 | except Exception as e: 482 | logger.error(f"Error in get_account_assignments: {str(e)}") 483 | return [], [] 484 | 485 | 486 | def create_directory_if_not_exists(path): 487 | """Create directory if it doesn't exist""" 488 | logger.info(f"Checking if directory exists: {path}") 489 | if not os.path.exists(path): 490 | os.makedirs(path) 491 | 492 | 493 | def write_json_file(data, filepath): 494 | """Write data to a JSON file with proper formatting""" 495 | logger.info(f"Writing JSON file: {filepath}") 496 | with open(filepath, 'w') as f: 497 | json.dump(data, f, indent=4) 498 | 499 | 500 | def main(): 501 | try: 502 | # Initialize global variables for delegated admin status and management account ID 503 | initialize_global_vars() 504 | 505 | base_dir = "identity-center-mapping-info" 506 | perm_sets_dir = os.path.join(base_dir, "permission-sets") 507 | create_directory_if_not_exists(perm_sets_dir) 508 | 509 | # get all permission sets and create individual JSON files 510 | perm_sets_response = ic_admin.list_permission_sets( 511 | InstanceArn=ic_instance_arn, 512 | MaxResults=100 513 | ) 514 | sleep(0.1) 515 | all_perm_sets = perm_sets_response['PermissionSets'] 516 | 517 | while 'NextToken' in perm_sets_response: 518 | perm_sets_response = ic_admin.list_permission_sets( 519 | InstanceArn=ic_instance_arn, 520 | NextToken=perm_sets_response['NextToken'], 521 | MaxResults=100 522 | ) 523 | all_perm_sets.extend(perm_sets_response['PermissionSets']) 524 | sleep(0.1) 525 | 526 | # Get assignments 527 | global_mapping, target_mapping = get_account_assignments() 528 | 529 | # Create permission set files, skipping those provisioned in management account 530 | for perm_set_arn in all_perm_sets: 531 | if not (IS_DELEGATED and perm_set_arn in skip_management_perm_sets): 532 | perm_set_json = get_permission_set_details(perm_set_arn) 533 | perm_set_file = os.path.join( 534 | perm_sets_dir, f"{perm_set_json['Name']}.json") 535 | write_json_file(perm_set_json, perm_set_file) 536 | logger.info(f"Created permission set file: {perm_set_file}") 537 | 538 | global_mapping_file = os.path.join(base_dir, "global-mapping.json") 539 | write_json_file(global_mapping, global_mapping_file) 540 | logger.info(f"Created global mapping file: {global_mapping_file}") 541 | 542 | target_mapping_file = os.path.join(base_dir, "target-mapping.json") 543 | write_json_file(target_mapping, target_mapping_file) 544 | logger.info(f"Created target mapping file: {target_mapping_file}") 545 | 546 | except Exception as e: 547 | logger.error(f"Error in main execution: {str(e)}") 548 | raise 549 | 550 | 551 | if __name__ == "__main__": 552 | main() 553 | -------------------------------------------------------------------------------- /templates/codepipeline-stack.template: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Description: "This template creates AWS CodePipeline and other CICD resources for Identity Center automation solution (uksb-8ggfy7rjoj)." 4 | 5 | Conditions: 6 | IsCodeConnectionSource: !Equals [!Ref SourceType, 'CodeConnection'] 7 | IsCodeCommitSource: !Equals [!Ref SourceType, 'CodeCommit'] 8 | CreateMappingGeneratorProject: !Equals [!Ref GeneratePermissionSetsAndMappings, 'true'] 9 | Parameters: 10 | GeneratePermissionSetsAndMappings: 11 | Type: String 12 | Default: 'false' 13 | AllowedValues: 14 | - 'true' 15 | - 'false' 16 | Description: Set to true if you want to deploy the build project to generate permission sets and mapping JSON files, ONLY if you are already using Identity Center and would like to import existing permission sets and assignments into this solution. You must manually run the build project to generate permission sets and mapping files before pushing code to your source code repository. Follow README for more information. 17 | ICInstanceARN: 18 | Type: String 19 | Description: Required when GeneratePermissionSetsAndMappings is set to true. ICInstanceARN can be found on the AWS IAM Identity Center console 'Settings' page. 20 | IdentityStoreId: 21 | Type: String 22 | Description: Required when GeneratePermissionSetsAndMappings is set to true. Identity Store ID can be found on the AWS IAM Identity Center console 'Settings' page. 23 | SourceType: 24 | Description: Choose the source type for your pipeline (AWS CodeCommit or CodeConnection). 25 | Type: String 26 | Default: CodeConnection 27 | AllowedValues: 28 | - CodeConnection 29 | - CodeCommit 30 | RepositoryName: 31 | Description: The name of the repository (full name of repository with path for CodeConnection; repository name for AWS CodeCommit). 32 | Type: String 33 | Default: ic-automation 34 | RepoBranch: 35 | Description: The name of branch that will trigger the CodePipeline run. 36 | Type: String 37 | Default: main 38 | ConnectionArn: 39 | Description: The ARN of the CodeConnection Connection (required if SourceType is CodeConnection). 40 | Type: String 41 | Default: '' 42 | OrgManagementAccount: 43 | Type: String 44 | Description: Account ID of the management account. Used for setting up permissions for mapping automation project to read account names and OU names. 45 | AutomationBuildProjectName: 46 | Description: The full name of the automation CodeBuild Project. 47 | Type: String 48 | Default: ic-automation-build-project 49 | SNSPipelineApprovalEmail: 50 | Description: The email that will receive and approve pipeline approval notifications. 51 | Type: String 52 | ICMappingBucketName: 53 | Type: String 54 | Description: >- 55 | The same bucket name will be used in the automation and s3 stack. This S3 56 | bucket stores the permission sets and mapping files. Specify the same name you have specified in identity-center-stacks-parameters.json 57 | Default: ic-sso-automation 58 | 59 | Resources: 60 | MappingFilesS3Bucket: 61 | Type: 'AWS::S3::Bucket' 62 | Condition: CreateMappingGeneratorProject 63 | DeletionPolicy: Retain 64 | Properties: 65 | BucketName: !Sub icpermsetmapping-${AWS::AccountId}-${AWS::Region} 66 | NotificationConfiguration: 67 | EventBridgeConfiguration: 68 | EventBridgeEnabled: true 69 | VersioningConfiguration: 70 | Status: Enabled 71 | BucketEncryption: 72 | ServerSideEncryptionConfiguration: 73 | - ServerSideEncryptionByDefault: 74 | SSEAlgorithm: AES256 75 | 76 | MappingFilesS3BucketEventRule: 77 | Type: AWS::Events::Rule 78 | Condition: CreateMappingGeneratorProject 79 | Properties: 80 | Description: "Rule to trigger CodeBuild project when auto-generate-permissionsets-mapping-files.py is uploaded" 81 | EventPattern: 82 | source: 83 | - aws.s3 84 | detail-type: 85 | - "Object Created" 86 | detail: 87 | bucket: 88 | name: 89 | - !Ref MappingFilesS3Bucket 90 | object: 91 | key: 92 | - "auto-generate-permissionsets-mapping-files.py" 93 | State: ENABLED 94 | Targets: 95 | - Arn: !GetAtt GenerateMappingCodeBuildProject.Arn 96 | Id: "TriggerCodeBuildProject" 97 | RoleArn: !GetAtt EventBridgeCodeBuildRole.Arn 98 | 99 | EventBridgeCodeBuildRole: 100 | Type: AWS::IAM::Role 101 | Condition: CreateMappingGeneratorProject 102 | Properties: 103 | AssumeRolePolicyDocument: 104 | Version: '2012-10-17' 105 | Statement: 106 | - Effect: Allow 107 | Principal: 108 | Service: events.amazonaws.com 109 | Action: sts:AssumeRole 110 | Policies: 111 | - PolicyName: StartCodeBuildProject 112 | PolicyDocument: 113 | Version: '2012-10-17' 114 | Statement: 115 | - Effect: Allow 116 | Action: 117 | - codebuild:StartBuild 118 | Resource: !GetAtt GenerateMappingCodeBuildProject.Arn 119 | 120 | MappingCodeBuildServiceRole: 121 | Type: 'AWS::IAM::Role' 122 | Condition: CreateMappingGeneratorProject 123 | Properties: 124 | AssumeRolePolicyDocument: 125 | Version: '2012-10-17' 126 | Statement: 127 | - Effect: Allow 128 | Principal: 129 | Service: codebuild.amazonaws.com 130 | Action: 'sts:AssumeRole' 131 | ManagedPolicyArns: 132 | - 'arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess' 133 | Policies: 134 | - PolicyName: CodeBuildServiceRolePolicy 135 | PolicyDocument: 136 | Version: '2012-10-17' 137 | Statement: 138 | - Effect: Allow 139 | Action: 140 | - 's3:PutObject' 141 | - 's3:GetObject' 142 | - 's3:ListBucket' 143 | - 's3:DeleteObject' 144 | Resource: 145 | - !Sub "arn:aws:s3:::icpermsetmapping-${AWS::AccountId}-${AWS::Region}/*" 146 | - !Sub "arn:aws:s3:::icpermsetmapping-${AWS::AccountId}-${AWS::Region}" 147 | - Effect: Allow 148 | Action: 149 | - "sso:DescribePermissionSet" 150 | - "sso:DescribePermissionSetProvisioningStatus" 151 | - "sso:DescribePermissionsPolicies" 152 | - "sso:DescribeRegisteredRegions" 153 | - "sso:GetInlinePolicyForPermissionSet" 154 | - "sso:GetPermissionSet" 155 | - "sso:GetPermissionsBoundaryForPermissionSet" 156 | - "sso:GetPermissionsPolicy" 157 | - "sso:ListAccountAssignments" 158 | - "sso:ListAccountsForProvisionedPermissionSet" 159 | - "sso:ListCustomerManagedPolicyReferencesInPermissionSet" 160 | - "sso:ListManagedPoliciesInPermissionSet" 161 | - "sso:ListPermissionSetProvisioningStatus" 162 | - "sso:ListPermissionSets" 163 | - "sso:ListPermissionSetsProvisionedToAccount" 164 | - "sso:ListTagsForResource" 165 | - "identitystore:DescribeGroup" 166 | - "organizations:ListAccounts" 167 | - "organizations:ListRoots" 168 | - "organizations:DescribeOrganization" 169 | - "identitystore:ListGroups" 170 | - "identitystore:GetGroupId" 171 | - "iam:GetRole" 172 | - "iam:GetSAMLProvider" 173 | - "iam:ListAttachedRolePolicies" 174 | - "iam:ListRolePolicies" 175 | Resource: '*' 176 | - Effect: Allow 177 | Action: 178 | - 'logs:CreateLogGroup' 179 | - 'logs:CreateLogStream' 180 | - 'logs:PutLogEvents' 181 | Resource: '*' 182 | - Sid: OrganizationsActions 183 | Effect: Allow 184 | Action: 185 | - "organizations:ListAccountsForParent" 186 | - "organizations:DescribeAccount" 187 | - "organizations:ListChildren" 188 | - "organizations:ListOrganizationalUnitsForParent" 189 | - "organizations:ListParents" 190 | - "organizations:DescribeOrganizationalUnit" 191 | Resource: 192 | - !Sub "arn:aws:organizations::${OrgManagementAccount}:ou/o-*/ou-*" 193 | - !Sub "arn:aws:organizations::${OrgManagementAccount}:account/o-*/*" 194 | - !Sub "arn:aws:organizations::${OrgManagementAccount}:root/o-*/r-*" 195 | GenerateMappingCodeBuildProject: 196 | Type: 'AWS::CodeBuild::Project' 197 | Condition: CreateMappingGeneratorProject 198 | Properties: 199 | Name: 'IC-GeneratePermissionSetsAndMappingFiles' 200 | Description: 'CodeBuild project to generate Identity Center permission set mapping files' 201 | ServiceRole: !GetAtt MappingCodeBuildServiceRole.Arn 202 | Artifacts: 203 | Type: NO_ARTIFACTS 204 | Environment: 205 | Type: LINUX_CONTAINER 206 | ComputeType: BUILD_GENERAL1_SMALL 207 | Image: aws/codebuild/standard:7.0 208 | EnvironmentVariables: 209 | - Name: IDENTITY_STORE_ID 210 | Type: PLAINTEXT 211 | Value: !Ref IdentityStoreId 212 | - Name: IC_INSTANCE_ARN 213 | Type: PLAINTEXT 214 | Value: !Ref ICInstanceARN 215 | - Name: S3_BUCKET_NAME 216 | Type: PLAINTEXT 217 | Value: !Sub ICPermSetMapping-${AWS::AccountId}-${AWS::Region} 218 | - Name: AWS_REGION 219 | Type: PLAINTEXT 220 | Value: !Sub ${AWS::Region} 221 | 222 | Source: 223 | Type: NO_SOURCE 224 | BuildSpec: !Sub | 225 | version: 0.2 226 | phases: 227 | pre_build: 228 | commands: 229 | - export DEBIAN_FRONTEND=noninteractive 230 | - apt update 231 | - apt-get install python3-pip -y -q 232 | - apt-get install jq git -y -q 233 | - pip3 install boto3 --quiet 234 | - aws s3 cp s3://icpermsetmapping-${AWS::AccountId}-${AWS::Region}/auto-generate-permissionsets-mapping-files.py . 235 | build: 236 | commands: 237 | - echo "Running permission set and mapping files generator" 238 | - python3 auto-generate-permissionsets-mapping-files.py 239 | - echo "Syncing generated mapping files to S3" 240 | - aws s3 sync identity-center-mapping-info/ s3://icpermsetmapping-${AWS::AccountId}-${AWS::Region}/identity-center-mapping-info/ --delete 241 | - aws s3api list-objects-v2 --bucket icpermsetmapping-${AWS::AccountId}-${AWS::Region} 242 | - echo "Generation and sync complete" 243 | artifacts: 244 | files: 245 | - '**/*' 246 | TimeoutInMinutes: 120 247 | ValidationProject: 248 | Type: AWS::CodeBuild::Project 249 | Properties: 250 | Name: IC-Syntax-Validation 251 | Description: Validates syntax of permission sets and mapping files 252 | ServiceRole: !GetAtt CodeBuildRole.Arn 253 | Artifacts: 254 | Type: CODEPIPELINE 255 | Environment: 256 | Type: LINUX_CONTAINER 257 | ComputeType: BUILD_GENERAL1_SMALL 258 | Image: aws/codebuild/standard:7.0 259 | Source: 260 | Type: CODEPIPELINE 261 | BuildSpec: src/codebuild/buildspec-validation.yml 262 | Location: !If 263 | - IsCodeCommitSource 264 | - !Sub https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${RepositoryName} 265 | - !Ref AWS::NoValue 266 | SourceVersion: !Sub 'refs/heads/${RepoBranch}' 267 | TimeoutInMinutes: 10 268 | PipelineArtifactStoreBucket: 269 | Type: 'AWS::S3::Bucket' 270 | DeletionPolicy: Delete 271 | Properties: 272 | PublicAccessBlockConfiguration: 273 | BlockPublicAcls: true 274 | BlockPublicPolicy: true 275 | IgnorePublicAcls: true 276 | RestrictPublicBuckets: true 277 | BucketEncryption: 278 | ServerSideEncryptionConfiguration: 279 | - BucketKeyEnabled: false 280 | ServerSideEncryptionByDefault: 281 | SSEAlgorithm: "AES256" 282 | Metadata: 283 | cfn_nag: 284 | rules_to_suppress: 285 | - id: W35 286 | reason: "In this AWS Sample, access logging for pipeline artifact bucket is optional" 287 | 288 | PipelineArtifactStoreBucketPolicy: 289 | Type: 'AWS::S3::BucketPolicy' 290 | Properties: 291 | Bucket: !Ref PipelineArtifactStoreBucket 292 | PolicyDocument: 293 | Version: 2012-10-17 294 | Statement: 295 | - Sid: DenyInsecureConnections 296 | Effect: Deny 297 | Principal: '*' 298 | Action: 's3:*' 299 | Resource: !Join 300 | - '' 301 | - - 'Fn::GetAtt': 302 | - PipelineArtifactStoreBucket 303 | - Arn 304 | - /* 305 | Condition: 306 | Bool: 307 | 'aws:SecureTransport': false 308 | - Sid: DenyOther 309 | Effect: Deny 310 | Principal: '*' 311 | Action: 312 | - "s3:GetObject" 313 | - "s3:GetObjectVersion" 314 | - "s3:PutObject" 315 | Resource: !Join 316 | - '' 317 | - - 'Fn::GetAtt': 318 | - PipelineArtifactStoreBucket 319 | - Arn 320 | - /* 321 | Condition: 322 | StringNotEquals: 323 | 'aws:PrincipalArn': 324 | - !GetAtt CodePipelineServiceRole.Arn 325 | - !GetAtt CodeBuildRole.Arn 326 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICPermissionSetAssignmentAutomationRole" 327 | SNSPipelineApprovalTopic: 328 | Type: AWS::SNS::Topic 329 | Properties: 330 | DisplayName: IC-Pipeline-Approval-Topic 331 | TopicName: IC-Pipeline-Approval-Topic 332 | KmsMasterKeyId: alias/aws/sns 333 | Subscription: 334 | - Endpoint: !Ref SNSPipelineApprovalEmail 335 | Protocol: email 336 | 337 | SNSPipelineApprovalTopicPolicy: 338 | Type: AWS::SNS::TopicPolicy 339 | Properties: 340 | Topics: 341 | - !Ref SNSPipelineApprovalTopic 342 | PolicyDocument: 343 | Statement: 344 | - Sid: AWSSNSPolicy 345 | Action: 346 | - sns:Publish 347 | Effect: Allow 348 | Resource: !Ref SNSPipelineApprovalTopic 349 | Principal: 350 | AWS: 351 | - !GetAtt CodePipelineServiceRole.Arn 352 | 353 | AWSCodePipeline: 354 | Type: 'AWS::CodePipeline::Pipeline' 355 | Properties: 356 | PipelineType: V2 357 | ArtifactStore: 358 | Location: !Ref PipelineArtifactStoreBucket 359 | Type: S3 360 | RoleArn: !GetAtt CodePipelineServiceRole.Arn 361 | Name: Identity-Center-Automation-Solution 362 | Stages: 363 | - Name: Source 364 | Actions: 365 | - !If 366 | - IsCodeConnectionSource 367 | - Name: CodeConnectionSource 368 | ActionTypeId: 369 | Category: Source 370 | Owner: AWS 371 | Version: '1' 372 | Provider: CodeStarSourceConnection 373 | Configuration: 374 | ConnectionArn: !Ref ConnectionArn 375 | FullRepositoryId: !Ref RepositoryName 376 | BranchName: !Ref RepoBranch 377 | OutputArtifacts: 378 | - Name: SourceArtifacts 379 | Namespace: SourceVariables 380 | RunOrder: 1 381 | - Name: CodeCommitSource 382 | ActionTypeId: 383 | Category: Source 384 | Owner: AWS 385 | Version: '1' 386 | Provider: CodeCommit 387 | Configuration: 388 | RepositoryName: !Ref RepositoryName 389 | BranchName: !Ref RepoBranch 390 | PollForSourceChanges: false 391 | OutputArtifacts: 392 | - Name: SourceArtifacts 393 | Namespace: SourceVariables 394 | RunOrder: 1 395 | - Name: ValidateAndPackage 396 | Actions: 397 | - Name: ParseandScan 398 | InputArtifacts: 399 | - Name: SourceArtifacts 400 | ActionTypeId: 401 | Category: Build 402 | Owner: AWS 403 | Provider: CodeBuild 404 | Version: '1' 405 | Configuration: 406 | ProjectName: !Ref CreateParameterFiles 407 | RunOrder: 1 408 | OutputArtifacts: 409 | - Name: UpdatedSourceArtifacts 410 | - Name: MappingFilesSyntaxValidation 411 | InputArtifacts: 412 | - Name: SourceArtifacts 413 | ActionTypeId: 414 | Category: Build 415 | Owner: AWS 416 | Version: '1' 417 | Provider: CodeBuild 418 | Configuration: 419 | ProjectName: !Ref ValidationProject 420 | RunOrder: 1 421 | - Name: BuildS3Stack 422 | ActionTypeId: 423 | Category: Deploy 424 | Owner: AWS 425 | Version: '1' 426 | Provider: CloudFormation 427 | Configuration: 428 | ActionMode: CREATE_UPDATE 429 | StackName: IdentityCenter-S3-Bucket-Stack 430 | Capabilities: CAPABILITY_IAM 431 | TemplateConfiguration: UpdatedSourceArtifacts::ic-s3-parameters.json 432 | TemplatePath: UpdatedSourceArtifacts::templates/identity-center-s3-bucket.template 433 | RoleArn: !GetAtt CodeBuildRole.Arn 434 | InputArtifacts: 435 | - Name: UpdatedSourceArtifacts 436 | RunOrder: 2 437 | - Name: CreateSourceZipCode 438 | InputArtifacts: 439 | - Name: UpdatedSourceArtifacts 440 | ActionTypeId: 441 | Category: Build 442 | Owner: AWS 443 | Provider: CodeBuild 444 | Version: '1' 445 | Configuration: 446 | ProjectName: !Ref CodeBuildProjectS3Upload 447 | RunOrder: 3 448 | OutputArtifacts: 449 | - Name: UpdatedSourceArtifactsWithObjectVersion 450 | - Name: BuildICTemplateAndSyncMappingFiles 451 | Actions: 452 | - Name: BuildICTemplate 453 | ActionTypeId: 454 | Category: Deploy 455 | Owner: AWS 456 | Version: '1' 457 | Provider: CloudFormation 458 | InputArtifacts: 459 | - Name: UpdatedSourceArtifactsWithObjectVersion 460 | Configuration: 461 | ActionMode: CREATE_UPDATE 462 | StackName: IdentityCenter-Automation-Stack 463 | Capabilities: 'CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND' 464 | ParameterOverrides: !Sub '{"AutomationBuildProjectName": "${AutomationBuildProjectName}"}' 465 | TemplateConfiguration: UpdatedSourceArtifactsWithObjectVersion::ic-automation-parameters.json 466 | TemplatePath: UpdatedSourceArtifactsWithObjectVersion::templates/identity-center-automation.template 467 | RoleArn: !GetAtt CodeBuildRole.Arn 468 | RunOrder: 1 469 | - Name: SyncMappingFiles 470 | InputArtifacts: 471 | - Name: UpdatedSourceArtifacts 472 | ActionTypeId: 473 | Category: Build 474 | Owner: AWS 475 | Provider: CodeBuild 476 | Version: '1' 477 | Configuration: 478 | ProjectName: !Ref UploadMappingFiles 479 | RunOrder: 2 480 | - Name: ReviewAndExecute 481 | Actions: 482 | - Name: Approval 483 | ActionTypeId: 484 | Category: Approval 485 | Owner: AWS 486 | Version: '1' 487 | Provider: Manual 488 | Configuration: 489 | NotificationArn: !Ref SNSPipelineApprovalTopic 490 | CustomData: !If 491 | - IsCodeConnectionSource 492 | - "Author #{SourceVariables.AuthorId} made Commit #{SourceVariables.CommitId}. Message: #{SourceVariables.CommitMessage}." 493 | - "Author made Commit #{SourceVariables.CommitId}. Message: #{SourceVariables.CommitMessage}." 494 | # CustomData: "Author #{SourceVariables.AuthorId} made Commit #{SourceVariables.CommitId}.\n\nMessage: #{SourceVariables.CommitMessage}." 495 | # ExternalEntityLink: !Sub "https://github.com/${RepositoryName}/pull/#{Source.BranchName}" 496 | RunOrder: 1 497 | - Name: TriggerCodeBuildAutomation 498 | InputArtifacts: 499 | - Name: UpdatedSourceArtifacts 500 | ActionTypeId: 501 | Category: Build 502 | Owner: AWS 503 | Provider: CodeBuild 504 | Version: '1' 505 | Configuration: 506 | ProjectName: !Ref AutomationBuildProjectName 507 | EnvironmentVariables: | 508 | [ 509 | { 510 | "name": "CODEPIPELINE_EXECUTION_ID", 511 | "value": "#{codepipeline.PipelineExecutionId}", 512 | "type": "PLAINTEXT" 513 | }, 514 | { 515 | "name": "COMMIT_ID", 516 | "value": "#{SourceVariables.CommitId}", 517 | "type": "PLAINTEXT" 518 | } 519 | ] 520 | RunOrder: 2 521 | 522 | CodePipelineServiceRole: 523 | Type: 'AWS::IAM::Role' 524 | Properties: 525 | RoleName: !Sub ICPipelineRole-${AWS::AccountId} 526 | AssumeRolePolicyDocument: 527 | Statement: 528 | - Action: 529 | - 'sts:AssumeRole' 530 | Effect: Allow 531 | Principal: 532 | Service: 533 | - codepipeline.amazonaws.com 534 | Version: 2012-10-17 535 | Path: / 536 | Policies: 537 | - PolicyName: Pipeline-Service-Policy 538 | PolicyDocument: 539 | Version: 2012-10-17 540 | Statement: 541 | - !If 542 | - IsCodeConnectionSource 543 | - Action: 544 | - 'codestar-connections:UseConnection' 545 | - 'codeconnections:UseConnection' 546 | Effect: 'Allow' 547 | Resource: !Ref ConnectionArn 548 | - !Ref AWS::NoValue 549 | - Action: 'sns:Publish' 550 | Effect: 'Allow' 551 | Resource: !Ref SNSPipelineApprovalTopic 552 | - Effect: Allow 553 | Action: 554 | - 'codecommit:CancelUploadArchive' 555 | - 'codecommit:GetBranch' 556 | - 'codecommit:GetCommit' 557 | - 'codecommit:GetUploadArchiveStatus' 558 | - 'codecommit:UploadArchive' 559 | Resource: '*' 560 | - Effect: Allow 561 | Action: 562 | - 'codedeploy:CreateDeployment' 563 | - 'codedeploy:GetApplicationRevision' 564 | - 'codedeploy:GetDeployment' 565 | - 'codedeploy:GetDeploymentConfig' 566 | - 'codedeploy:RegisterApplicationRevision' 567 | Resource: '*' 568 | - Effect: Allow 569 | Action: 570 | - 'codebuild:BatchGetBuilds' 571 | - 'codebuild:StartBuild' 572 | Resource: '*' 573 | - Effect: Allow 574 | Action: 575 | - 'lambda:InvokeFunction' 576 | - 'lambda:ListFunctions' 577 | Resource: '*' 578 | - Effect: Allow 579 | Action: 580 | - 'iam:PassRole' 581 | Resource: 582 | - !GetAtt CodeBuildRole.Arn 583 | - Effect: Allow 584 | Action: 585 | - 'cloudformation:CreateStack' 586 | - 'cloudformation:DescribeStacks' 587 | - 'cloudformation:UpdateStack' 588 | Resource: '*' 589 | - Effect: Allow 590 | Action: 591 | - "s3:GetObject" 592 | - "s3:GetObjectVersion" 593 | - "s3:PutObject" 594 | Resource: !Join 595 | - '' 596 | - - 'Fn::GetAtt': 597 | - PipelineArtifactStoreBucket 598 | - Arn 599 | - /* 600 | 601 | CreateParameterFiles: 602 | Type: 'AWS::CodeBuild::Project' 603 | Properties: 604 | Name: ParseandScan 605 | Description: Use input json file to create the CFN parameter files for both CFN stack. 606 | # EncryptionKey: alias/aws/s3 607 | ServiceRole: !GetAtt CodeBuildRole.Arn 608 | Artifacts: 609 | Type: CODEPIPELINE 610 | Environment: 611 | Type: LINUX_CONTAINER 612 | ComputeType: BUILD_GENERAL1_SMALL 613 | Image: 'aws/codebuild/standard:7.0' 614 | Source: 615 | # Type: !If [IsCodeCommitSource, 'CODECOMMIT', 'CODEPIPELINE'] 616 | Type: CODEPIPELINE 617 | BuildSpec: src/codebuild/buildspec-param.yml 618 | Location: !If 619 | - IsCodeCommitSource 620 | - !Sub https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${RepositoryName} 621 | - !Ref AWS::NoValue 622 | SourceVersion: !Sub 'refs/heads/${RepoBranch}' 623 | TimeoutInMinutes: 20 624 | 625 | CodeBuildProjectS3Upload: 626 | Type: 'AWS::CodeBuild::Project' 627 | Properties: 628 | Name: BuildandUploadICAutomationSourceCodeFiles 629 | Description: Compress automation python scripts and sync the zip files to Identity Center secure S3 bucket. 630 | # EncryptionKey: alias/aws/s3 631 | ServiceRole: !GetAtt CodeBuildRole.Arn 632 | Artifacts: 633 | Type: CODEPIPELINE 634 | Environment: 635 | Type: LINUX_CONTAINER 636 | ComputeType: BUILD_GENERAL1_SMALL 637 | Image: 'aws/codebuild/standard:7.0' 638 | EnvironmentVariables: 639 | - Name: S3_BUCKET_NAME 640 | Type: PLAINTEXT 641 | Value: !Join 642 | - '-' 643 | - - !Ref ICMappingBucketName 644 | - !Sub ${AWS::AccountId} 645 | - !Sub ${AWS::Region} 646 | Source: 647 | Type: CODEPIPELINE 648 | 649 | BuildSpec: src/codebuild/buildspec-zipfiles.yml 650 | SourceVersion: !Sub 'refs/heads/${RepoBranch}' 651 | TimeoutInMinutes: 20 652 | 653 | UploadMappingFiles: 654 | Type: 'AWS::CodeBuild::Project' 655 | Properties: 656 | Name: UploadMappingFilestoS3 657 | Description: >- 658 | The mapping files are ready for build. This will sync the latest Identity Center mapping 659 | definition files to s3. 660 | # EncryptionKey: alias/aws/s3 661 | ServiceRole: !GetAtt CodeBuildRole.Arn 662 | Artifacts: 663 | Type: CODEPIPELINE 664 | Environment: 665 | Type: LINUX_CONTAINER 666 | ComputeType: BUILD_GENERAL1_SMALL 667 | Image: 'aws/codebuild/standard:7.0' 668 | EnvironmentVariables: 669 | - Name: S3_BUCKET_NAME 670 | Type: PLAINTEXT 671 | Value: !Join 672 | - '-' 673 | - - !Ref ICMappingBucketName 674 | - !Sub ${AWS::AccountId} 675 | - !Sub ${AWS::Region} 676 | Source: 677 | # Type: !If [IsCodeCommitSource, 'CODECOMMIT', 'CODEPIPELINE'] 678 | Type: CODEPIPELINE 679 | BuildSpec: src/codebuild/buildspec-mapping.yml 680 | Location: !If 681 | - IsCodeCommitSource 682 | - !Sub https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${RepositoryName} 683 | - !Ref AWS::NoValue 684 | TimeoutInMinutes: 20 685 | 686 | CodeBuildRole: 687 | Type: 'AWS::IAM::Role' 688 | Properties: 689 | RoleName: ICAutoPipelineCodeBuildRole 690 | AssumeRolePolicyDocument: 691 | Statement: 692 | - Action: 693 | - 'sts:AssumeRole' 694 | Effect: Allow 695 | Principal: 696 | Service: 697 | - codebuild.amazonaws.com 698 | - cloudformation.amazonaws.com 699 | Version: 2012-10-17 700 | Path: / 701 | #https://docs.aws.amazon.com/codebuild/latest/userguide/auth-and-access-control-permissions-reference.html 702 | Policies: 703 | - PolicyName: CodeBuildAccess 704 | PolicyDocument: 705 | Version: 2012-10-17 706 | Statement: 707 | - !If 708 | - IsCodeCommitSource 709 | - Effect: Allow 710 | Action: 711 | - codecommit:GitPull 712 | - codecommit:GetBranch 713 | - codecommit:GetCommit 714 | - codecommit:GetRepository 715 | Resource: !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${RepositoryName} 716 | - !Ref AWS::NoValue 717 | # - Effect: Allow 718 | # Action: 719 | # - 'cloudwatch:*' 720 | # Resource: '*' 721 | - Effect: Allow 722 | Action: 723 | - 'kms:CreateAlias' 724 | - 'kms:CreateGrant' 725 | - 'kms:CreateKey' 726 | - 'kms:Decrypt' 727 | - 'kms:DeleteAlias' 728 | - 'kms:DescribeKey' 729 | - 'kms:EnableKeyRotation' 730 | - 'kms:Encrypt' 731 | - 'kms:GenerateDataKey' 732 | - 'kms:GetKeyPolicy' 733 | - 'kms:GetKeyRotationStatus' 734 | - 'kms:ListResourceTags' 735 | - 'kms:ScheduleKeyDeletion' 736 | Resource: '*' 737 | - Effect: Allow 738 | Action: 739 | - 'logs:*' 740 | Resource: '*' 741 | - Effect: Allow 742 | Action: 743 | - 'dynamodb:CreateTable' 744 | - 'dynamodb:DeleteTable' 745 | - 'dynamodb:DescribeContinuousBackups' 746 | - 'dynamodb:DescribeContributorInsights' 747 | - 'dynamodb:DescribeKinesisStreamingDestination' 748 | - 'dynamodb:DescribeTable' 749 | - 'dynamodb:DescribeTimeToLive' 750 | - 'dynamodb:ListTagsOfResource' 751 | - 'dynamodb:UpdateTable' 752 | - 'events:DeleteRule' 753 | - 'events:DescribeRule' 754 | - 'events:PutRule' 755 | - 'events:PutTargets' 756 | - 'events:RemoveTargets' 757 | - 'iam:CreateRole' 758 | - 'iam:DeleteRole' 759 | - 'iam:DeleteRolePolicy' 760 | - 'iam:GetRole' 761 | - 'iam:GetRolePolicy' 762 | - 'iam:PutRolePolicy' 763 | - 'lambda:AddPermission' 764 | - 'lambda:CreateFunction' 765 | - 'lambda:DeleteFunction' 766 | - 'lambda:GetFunction' 767 | - 'lambda:RemovePermission' 768 | - 'lambda:UpdateFunction*' 769 | - 'codebuild:CreateProject' 770 | - 'codebuild:DeleteProject' 771 | - 'codebuild:UpdateProject' 772 | - 'codebuild:StartBuild' 773 | - 'codebuild:ListProjects' 774 | - 'codebuild:ListBuilds' 775 | - 'codebuild:ListBuildsForProject' 776 | - 'codebuild:BatchGetBuilds' 777 | - 'codebuild:BatchGetProjects' 778 | - 'events:PutPermission' 779 | - 'events:RemovePermission' 780 | Resource: '*' 781 | - Effect: Allow 782 | Action: 783 | - 'iam:PassRole' 784 | Resource: 785 | # Role Names are defined in 'identity-center-automation.template' 786 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICPermissionSetAssignmentAutomationRole" 787 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/ICAlertSNSNotificationRole" 788 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/AutoICEventBridgeRole" 789 | #Condition: 790 | # StringEquals: 791 | # iam:PassedToService: 792 | # - codebuild.amazonaws.com 793 | - Effect: Allow 794 | Action: 795 | - 's3:CreateBucket' 796 | - 's3:DeleteBucket' 797 | - 's3:DeleteBucketPolicy' 798 | - 's3:DeleteObject' 799 | - 's3:DeleteObjects' 800 | - 's3:DeleteObjectTagging' 801 | - 's3:GetBucketPolicy' 802 | - 's3:GetObject' 803 | - 's3:GetObjectVersion' 804 | - 's3:ListBucket' 805 | - 's3:PutEncryptionConfiguration' 806 | - 's3:PutBucketEncryption' 807 | - 's3:PutBucketPolicy' 808 | - 's3:PutBucketPublicAccessBlock' 809 | - 's3:PutBucketVersioning' 810 | - 's3:PutObject' 811 | - 's3:PutObjectTagging' 812 | Resource: '*' 813 | - Effect: Allow 814 | Action: 815 | - 'sns:Subscribe' 816 | - 'sns:SetTopicAttributes' 817 | - 'sns:CreateTopic' 818 | - 'sns:GetTopicAttributes' 819 | - 'sns:DeleteTopic' 820 | - 'sns:Unsubscribe' 821 | - 'sns:ListSubscriptionsByTopic' 822 | Resource: '*' 823 | - Effect: Allow 824 | Action: 825 | - 'logs:CreateLogGroup' 826 | - 'logs:CreateLogStream' 827 | - 'logs:PutLogEvents' 828 | Resource: '*' 829 | - Effect: Allow 830 | Action: 831 | - 'events:DescribeEventBus' 832 | - 'events:CreateEventBus' 833 | - 'events:DeleteEventBus' 834 | - 'events:UpdateEventBus' 835 | - 'events:DeleteRule' 836 | - 'events:RemoveTargets' 837 | Resource: 838 | - !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/*" 839 | - !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/*" 840 | - !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/*/*" 841 | Metadata: 842 | cfn_nag: 843 | rules_to_suppress: 844 | - id: F3 845 | reason: "Wildcard just for the log group permissions" 846 | Outputs: 847 | MappingFilesS3BucketName: 848 | Condition: CreateMappingGeneratorProject 849 | Description: Name of the S3 bucket storing Identity Center mapping files 850 | Value: !Ref MappingFilesS3Bucket 851 | CodeBuildProjectName: 852 | Condition: CreateMappingGeneratorProject 853 | Description: Name of the CodeBuild project for generating mapping files 854 | Value: !Ref GenerateMappingCodeBuildProject 855 | 856 | -------------------------------------------------------------------------------- /src/validation/syntax-validator.py: -------------------------------------------------------------------------------- 1 | """Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved""" 2 | import os 3 | import sys 4 | import json 5 | import logging 6 | import re 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | # Stream handler to print logs on screen 12 | console_handler = logging.StreamHandler() 13 | console_handler.setLevel(logging.INFO) 14 | 15 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 16 | console_handler.setFormatter(formatter) 17 | logger.addHandler(console_handler) 18 | 19 | 20 | def log_and_append_error(message, errors, error_type="SYNTAX"): 21 | """ 22 | Log an error message and append it to the errors list. 23 | """ 24 | formatted_message = f"[{error_type}] {message}" 25 | logger.error(formatted_message) 26 | errors.append(formatted_message) 27 | 28 | 29 | def is_valid_arn(arn: str) -> bool: 30 | """Validate AWS IAM policy ARN format""" 31 | if not arn: 32 | return False 33 | pattern = r'^arn:aws:iam::(?:\d{12}|aws):policy/(?:job-function/|service-role/)?[a-zA-Z0-9+=,.@-_/]+$' 34 | return bool(re.match(pattern, arn)) 35 | 36 | 37 | def is_valid_iam_role_arn(arn: str) -> bool: 38 | """ 39 | Validate AWS IAM Role ARN format. 40 | Role name must be 1-64 characters using alphanumeric and '+=,.@-_' characters. 41 | Supports AWS service-linked roles and Identity Center (SSO) reserved roles. 42 | """ 43 | if not arn: 44 | return False 45 | 46 | pattern = ( 47 | r'^arn:aws:iam::\d{12}:role/' 48 | r'(?:aws-service-role/[a-z0-9.-]+\.amazonaws\.com/|' 49 | r'aws-reserved/sso\.amazonaws\.com/[a-z0-9-]+/)?' 50 | r'[a-zA-Z0-9+=,.@_-]{1,64}(?:/[a-zA-Z0-9+=,.@_-]{1,64})*$' 51 | ) 52 | 53 | return bool(re.match(pattern, arn)) 54 | 55 | 56 | def is_valid_ic_instance_arn(arn: str) -> bool: 57 | """Validate AWS Identity Center Instance ARN format""" 58 | if not arn: 59 | return False 60 | pattern = r'^arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):sso:::instance/(sso)?ins-[a-zA-Z0-9-.]{16}$' 61 | return bool(re.match(pattern, arn)) 62 | 63 | 64 | def is_valid_kms_key_arn(arn: str) -> bool: 65 | """Validate AWS KMS Key ARN format""" 66 | if not arn: 67 | return False 68 | pattern = r'^arn:aws:kms:[a-z0-9-]+:\d{12}:key/[a-f0-9-]{36}$' 69 | return bool(re.match(pattern, arn)) 70 | 71 | 72 | def validate_inline_policies(policies: dict) -> tuple[bool, list[str]]: 73 | """ 74 | Validate the complete inline policies structure. 75 | Returns (is_valid, list_of_errors). 76 | """ 77 | logger.info("Starting validation of inline policies") 78 | errors = [] 79 | 80 | # Validate structure 81 | if not isinstance(policies, dict): 82 | errors.append("InlinePolicy must be a dictionary") 83 | return False, errors 84 | 85 | # Validate Version 86 | if "Version" not in policies: 87 | errors.append("Policy must contain 'Version'") 88 | elif policies["Version"] != "2012-10-17": 89 | errors.append("Policy Version must be '2012-10-17'") 90 | 91 | # Validate Statement 92 | if "Statement" not in policies: 93 | errors.append("Policy must contain 'Statement'") 94 | return False, errors 95 | 96 | statements = policies["Statement"] 97 | if not isinstance(statements, list): 98 | errors.append("Statement must be a list") 99 | return False, errors 100 | 101 | for idx, statement in enumerate(statements): 102 | is_valid, error = validate_inline_policy_statement(statement) 103 | if not is_valid: 104 | errors.append(f"Invalid statement at index {idx}: {error}") 105 | 106 | # check policy size limit 107 | policy_json = json.dumps(policies) 108 | if len(policy_json) > 10240: 109 | errors.append( 110 | "Policy document exceeds maximum size of 10,240 characters") 111 | 112 | return len(errors) == 0, errors 113 | 114 | 115 | def validate_inline_policy_statement(statement: dict) -> tuple[bool, str]: 116 | """ 117 | Validate an IAM policy statement. 118 | Returns (is_valid, error_message). 119 | """ 120 | logger.info("Validating IAM policy statement") 121 | if not isinstance(statement, dict): 122 | return False, "Statement must be a dictionary" 123 | 124 | # validate Effect 125 | if "Effect" not in statement: 126 | return False, "Statement must contain 'Effect'" 127 | if statement["Effect"] not in ["Allow", "Deny"]: 128 | return False, "Effect must be either 'Allow' or 'Deny'" 129 | 130 | # Validate Action and Resource 131 | if "Action" not in statement and "NotAction" not in statement: 132 | return False, "Statement must contain either 'Action' or 'NotAction'" 133 | if "Resource" not in statement and "NotResource" not in statement: 134 | return False, "Statement must contain either 'Resource' or 'NotResource'" 135 | 136 | # Validate Sid if present 137 | if "Sid" in statement: 138 | sid = statement["Sid"] 139 | if not isinstance(sid, str): 140 | return False, "Sid must be a string" 141 | 142 | sid_pattern = r'^[a-zA-Z0-9]+$' 143 | if not re.match(sid_pattern, sid): 144 | return False, "Sid must only contain alphanumeric characters [a-z, A-Z, 0-9]" 145 | 146 | return True, "" 147 | 148 | 149 | def validate_duplicate_ps_names(base_path: str) -> list[str]: 150 | """ 151 | Verify if there are any duplicate permission set files. 152 | Returns a list of validation errors. 153 | """ 154 | logger.info("Checking for duplicate permission set names") 155 | errors = [] 156 | 157 | try: 158 | filenames = os.listdir(os.path.join(base_path, 'permission-sets')) 159 | seen_names = set() 160 | for filename in filenames: 161 | if filename.endswith('.json'): 162 | with open(os.path.join(base_path, 'permission-sets', filename), 'r') as f: 163 | ps = json.load(f) 164 | name = ps.get('Name') 165 | if name in seen_names: 166 | errors.append( 167 | f"Duplicate permission set name found: {name}") 168 | seen_names.add(name) 169 | 170 | except Exception as e: 171 | errors.append( 172 | f"Error during duplicate permission set name validation: {str(e)}") 173 | 174 | return errors 175 | 176 | 177 | def validate_whitespace(value: str, field_name: str) -> tuple[bool, str]: 178 | """Validate that a string value doesn't contain leading/trailing whitespace.""" 179 | if value != value.strip(): 180 | return False, f"{field_name} contains leading or trailing whitespace" 181 | return True, "" 182 | 183 | 184 | def contains_only_whitespace(value: str) -> bool: 185 | """Check if a string consists only of whitespace characters.""" 186 | return bool(value) and value.isspace() 187 | 188 | 189 | def validate_file_name_matches_content(file_name: str, content_name: str) -> bool: 190 | """ 191 | Validate that the JSON file name matches the Name field in content. 192 | Example: "admin-access.json" should contain {"Name": "admin-access"} 193 | """ 194 | logger.info( 195 | f"Validating if file name matches content: {file_name} vs {content_name}") 196 | base_name = file_name.rsplit('.', 1)[0] if '.' in file_name else file_name 197 | return base_name == content_name 198 | 199 | 200 | def verify_policy_name_matches_arn(name: str, arn: str) -> bool: 201 | """ 202 | Verify that policy name matches the last part of the ARN. 203 | Examples: 204 | - name="AdministratorAccess", arn="arn:aws:iam::aws:policy/AdministratorAccess" 205 | - name="SupportUser", arn="arn:aws:iam::aws:policy/job-function/SupportUser" 206 | """ 207 | try: 208 | arn_name = arn.split('/')[-1] 209 | return name == arn_name 210 | except: 211 | return False 212 | 213 | 214 | def validate_description(description: str) -> bool: 215 | """ 216 | Validate description format. 217 | Must be 1-700 characters, no control characters. 218 | """ 219 | if not description or len(description) > 700: 220 | return False 221 | return not any(ord(char) < 32 for char in description) 222 | 223 | 224 | def find_duplicate_policies(policies: list) -> set[str]: 225 | """ 226 | Find duplicate policy ARNs in a list of policies. 227 | Returns a set of duplicate ARNs. 228 | """ 229 | seen_arns = set() 230 | duplicates = set() 231 | 232 | for policy in policies: 233 | if "Arn" in policy: 234 | arn = policy["Arn"] 235 | if arn in seen_arns: 236 | duplicates.add(arn) 237 | seen_arns.add(arn) 238 | 239 | return duplicates 240 | 241 | 242 | def validate_session_duration(duration: str) -> bool: 243 | """ 244 | Validate ISO 8601 duration format for session duration. 245 | Must be in format PT[n]H where n is between 1-12. 246 | """ 247 | pattern = r'^PT([1-9]|1[0-2])H$' 248 | return bool(re.match(pattern, duration)) 249 | 250 | 251 | def validate_account_id(account_id: str) -> bool: 252 | """ 253 | Validate AWS account ID format. 254 | Must be 12 digits (no leading zeros) or "Global" for global mappings. 255 | """ 256 | if account_id == "Global": 257 | return True 258 | return bool(re.match(r'^\d{12}$', account_id)) 259 | 260 | 261 | def validate_ic_stacks_parameters(parameters: dict, errors: list, param_file) -> None: 262 | """Validate the identity-center-stacks-parameters.json file structure and content""" 263 | required_params = { 264 | 'AdminDelegated': str, 265 | 'ControlTowerEnabled': str, 266 | 'OrgManagementAccount': str, 267 | 'OrganizationId': str, 268 | 'IdentityStoreId': str, 269 | 'ICInstanceARN': str, 270 | 'ICMappingBucketName': str, 271 | 'SNSEmailEndpointSubscription': str, 272 | 'createICAdminRole': str, 273 | 'ICAutomationAdminArn': str, 274 | 'createICKMSAdminRole': str, 275 | 'ICKMSAdminArn': str, 276 | 'createS3KmsKey': str, 277 | 'S3KmsArn': str 278 | } 279 | 280 | required_non_empty_params = [ 281 | "OrgManagementAccount", 282 | "OrganizationId", 283 | "IdentityStoreId", 284 | "ICInstanceARN", 285 | "ICMappingBucketName", 286 | "SNSEmailEndpointSubscription" 287 | ] 288 | 289 | # Check if Parameters key exists 290 | if 'Parameters' not in parameters: 291 | log_and_append_error( 292 | f"Missing 'Parameters' key in {param_file}", errors) 293 | return 294 | 295 | for param in required_non_empty_params: 296 | if param not in parameters['Parameters']: 297 | log_and_append_error(f"Missing required parameter: {param}", errors) 298 | elif not parameters['Parameters'][param]: # Checks if value is empty string 299 | log_and_append_error(f"Parameter {param} cannot be empty", errors) 300 | 301 | params = parameters['Parameters'] 302 | 303 | # Check all required parameters exist and have correct type 304 | for param, param_type in required_params.items(): 305 | if param not in params: 306 | log_and_append_error( 307 | f"Missing required parameter '{param}' in {param_file}", errors) 308 | elif not isinstance(params[param], param_type): 309 | log_and_append_error( 310 | f"Parameter '{param}' must be of type {param_type.__name__} in {param_file}", errors) 311 | 312 | # Validate boolean values 313 | bool_params = ['AdminDelegated', 'ControlTowerEnabled', 314 | 'createICAdminRole', 'createICKMSAdminRole', 'createS3KmsKey'] 315 | for param in bool_params: 316 | if param in params and params[param] not in ['true', 'false']: 317 | log_and_append_error( 318 | f"Parameter '{param}' must be 'true' or 'false' in {param_file}", errors) 319 | 320 | # Validate Identity Center Instance ARN 321 | if params.get('ICInstanceARN') and not is_valid_ic_instance_arn(params['ICInstanceARN']): 322 | log_and_append_error( 323 | f"Invalid Identity Center Instance ARN format for parameter 'ICInstanceARN' in {param_file}", errors) 324 | 325 | # Validate IAM Role ARNs 326 | iam_role_arns = ['ICAutomationAdminArn', 'ICKMSAdminArn'] 327 | for param in iam_role_arns: 328 | if params.get(param) and not is_valid_iam_role_arn(params[param]): 329 | log_and_append_error( 330 | f"Invalid IAM Role ARN format for parameter '{param}' in {param_file}", errors) 331 | 332 | # Validate KMS Key ARN 333 | if params.get('S3KmsArn') and not is_valid_kms_key_arn(params['S3KmsArn']): 334 | log_and_append_error( 335 | f"Invalid KMS Key ARN format for parameter 'S3KmsArn' in {param_file}", errors) 336 | 337 | # Validate OrgManagementAccount (12 digit account ID) 338 | if params.get('OrgManagementAccount'): 339 | if not bool(re.match(r'^[0-9]{12}$', params['OrgManagementAccount'])): 340 | log_and_append_error( 341 | f"Invalid AWS account ID format for parameter 'OrgManagementAccount' in {param_file}. Must be 12 digits.", errors) 342 | 343 | # Validate OrganizationId (o-followed by 10-32 characters) 344 | if params.get('OrganizationId'): 345 | if not bool(re.match(r'^o-[a-z0-9]{10,32}$', params['OrganizationId'])): 346 | log_and_append_error( 347 | f"Invalid Organization ID format for parameter 'OrganizationId' in {param_file}. Must start with 'o-' followed by 10-32 alphanumeric characters.", errors) 348 | 349 | # Validate IdentityStoreId (10-32 character alphanumeric string) 350 | if params.get('IdentityStoreId'): 351 | if not bool(re.match(r'^[a-z0-9-]{10,32}$', params['IdentityStoreId'])): 352 | log_and_append_error( 353 | f"Invalid Identity Store ID format for parameter 'IdentityStoreId' in {param_file}. Must be 10-32 alphanumeric characters or hyphens.", errors) 354 | 355 | # Validate SNSEmailEndpointSubscription (valid email format) 356 | if params.get('SNSEmailEndpointSubscription'): 357 | if not bool(re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', params['SNSEmailEndpointSubscription'])): 358 | log_and_append_error( 359 | f"Invalid email format for parameter 'SNSEmailEndpointSubscription' in {param_file}.", errors) 360 | 361 | # Validate createICAdminRole and ICAutomationAdminArn relationship 362 | if params.get('createICAdminRole') == 'true' and params.get('ICAutomationAdminArn'): 363 | log_and_append_error( 364 | f"ICAutomationAdminArn must be empty when createICAdminRole is true in {param_file}", errors) 365 | elif params.get('ICAutomationAdminArn') and params.get('createICAdminRole') == 'true': 366 | log_and_append_error( 367 | f"createICAdminRole must be false when ICAutomationAdminArn has a value in {param_file}", errors) 368 | elif params.get('createICAdminRole') == 'false' and not params.get('ICAutomationAdminArn'): 369 | log_and_append_error( 370 | f"ICAutomationAdminArn must have a value when createICAdminRole is false in {param_file}", errors) 371 | 372 | # Validate createICKMSAdminRole and ICKMSAdminArn relationship 373 | if params.get('createICKMSAdminRole') == 'true' and params.get('ICKMSAdminArn'): 374 | log_and_append_error( 375 | f"ICKMSAdminArn must be empty when createICKMSAdminRole is true in {param_file}", errors) 376 | elif params.get('ICKMSAdminArn') and params.get('createICKMSAdminRole') == 'true': 377 | log_and_append_error( 378 | f"createICKMSAdminRole must be false when ICKMSAdminArn has a value in {param_file}", errors) 379 | elif params.get('createICKMSAdminRole') == 'false' and not params.get('ICKMSAdminArn'): 380 | log_and_append_error( 381 | f"ICKMSAdminArn must have a value when createICKMSAdminRole is false in {param_file}", errors) 382 | 383 | # Validate createS3KmsKey and S3KmsArn relationship 384 | if params.get('createS3KmsKey') == 'true' and params.get('S3KmsArn'): 385 | log_and_append_error( 386 | f"S3KmsArn must be empty when createS3KmsKey is true in {param_file}", errors) 387 | elif params.get('S3KmsArn') and params.get('createS3KmsKey') == 'true': 388 | log_and_append_error( 389 | f"createS3KmsKey must be false when S3KmsArn has a value in {param_file}", errors) 390 | elif params.get('createS3KmsKey') == 'false' and not params.get('S3KmsArn'): 391 | log_and_append_error( 392 | f"S3KmsArn must have a value when createS3KmsKey is false in {param_file}", errors) 393 | 394 | 395 | def validate_permission_set_name(name: str) -> bool: 396 | """ 397 | Validate permission set name. 398 | Must be 1-32 characters, alphanumeric and [-_] only. 399 | """ 400 | return bool(re.match(r'^[\w+=,.@-]{1,32}$', name)) 401 | 402 | 403 | def validate_tag_key(key: str) -> bool: 404 | """ 405 | Validate AWS tag key. 406 | Must be 1-128 characters and not start with aws:. 407 | """ 408 | if len(key) < 1 or len(key) > 128: 409 | return False 410 | return not key.lower().startswith(('aws:', 'amazon:', 'aws-')) 411 | 412 | 413 | def validate_tag_value(value: str) -> bool: 414 | """ 415 | Validate AWS tag value. 416 | Must be 0-256 characters. 417 | """ 418 | return len(value) <= 256 419 | 420 | 421 | def validate_permission_set_schema(permission_set, errors): 422 | """ 423 | Validate the permission set schema including format validations. 424 | Example of valid permission set: 425 | { 426 | "Name": "example-admin", 427 | "Description": "Admin access", 428 | "Session_Duration": "PT12H", 429 | "Tags": [{"Key": "env", "Value": "prod"}], 430 | "ManagedPolicies": [{"Name": "Admin", "Arn": "arn:aws:iam::aws:policy/Admin"}], 431 | "CustomerPolicies": [{"Name": "testPermissionBoundary", "Path": "/"}] 432 | "InlinePolicies": [], 433 | "PermissionsBoundary": { "Name": "testPermissionBoundary", "Path": "/"} 434 | } 435 | """ 436 | """ 437 | Validate the permission set schema including format validations. 438 | """ 439 | logger.info(f"Validating permission set schema for {permission_set['Name']}") 440 | 441 | required_keys = { 442 | "Name": str, 443 | "Description": str 444 | } 445 | 446 | optional_keys = { 447 | "Tags": list, 448 | "ManagedPolicies": list, 449 | "InlinePolicies": (list, dict), 450 | "CustomerPolicies": list, 451 | "Session_Duration": str, 452 | "PermissionsBoundary": dict 453 | } 454 | 455 | for key in permission_set.keys(): 456 | if key.lower() != key and key not in {"Name", "Description", "Session_Duration", "Tags", "ManagedPolicies", "CustomerPolicies", "InlinePolicies", "PermissionsBoundary"}: 457 | log_and_append_error( 458 | f"Inconsistent key casing: found '{key}' but expected standard casing", errors) 459 | 460 | permission_set_name = permission_set.get('Name', 'Unknown') 461 | if 'Description' in permission_set: 462 | if contains_only_whitespace(permission_set['Description']): 463 | log_and_append_error( 464 | f"Description in permission set {permission_set_name} cannot be only whitespace", errors) 465 | elif not validate_description(permission_set['Description']): 466 | log_and_append_error( 467 | f"Invalid Description in permission set {permission_set_name}. Must be 1-256 characters with no control characters", errors) 468 | 469 | # Check for duplicate policies 470 | duplicate_arns = find_duplicate_policies( 471 | permission_set.get("ManagedPolicies", [])) 472 | if duplicate_arns: 473 | log_and_append_error( 474 | f"Duplicate policy ARNs found in permission set {permission_set_name}: {', '.join(duplicate_arns)}", errors) 475 | 476 | # validate permission set name format 477 | if contains_only_whitespace(permission_set_name): 478 | log_and_append_error( 479 | f"Permission set name cannot be only whitespace", errors) 480 | elif not validate_permission_set_name(permission_set_name): 481 | log_and_append_error( 482 | f"Permission set name '{permission_set_name}' must be 1-32 characters, alphanumeric and [-_] only", errors) 483 | 484 | # validate session duration if present 485 | if 'Session_Duration' in permission_set: 486 | duration = permission_set['Session_Duration'] 487 | if not validate_session_duration(duration): 488 | log_and_append_error( 489 | f"Invalid Session_Duration format '{duration}' in permission set {permission_set_name}. Must be in format PT[n]H where n is 1-12", errors) 490 | 491 | # Validate required keys 492 | for key, expected_type in required_keys.items(): 493 | if key not in permission_set: 494 | log_and_append_error( 495 | f"Missing required key: {key} in permission set {permission_set_name}", errors) 496 | continue 497 | if not isinstance(permission_set[key], expected_type): 498 | log_and_append_error( 499 | f"Key '{key}' is not of expected type {expected_type.__name__} in permission set {permission_set_name}", errors) 500 | 501 | # Check for unknown keys 502 | allowed_keys = set(required_keys.keys()) | set(optional_keys.keys()) 503 | unknown_keys = set(permission_set.keys()) - allowed_keys 504 | if unknown_keys: 505 | log_and_append_error( 506 | f"Unknown keys found in permission set {permission_set_name}: {', '.join(unknown_keys)}", errors) 507 | 508 | # Validate PermissionsBoundary if present 509 | if 'PermissionsBoundary' in permission_set: 510 | boundary = permission_set['PermissionsBoundary'] 511 | if not isinstance(boundary, dict): 512 | log_and_append_error( 513 | f"PermissionsBoundary must be a dictionary in permission set {permission_set_name}", errors) 514 | else: 515 | if "Name" not in boundary: 516 | log_and_append_error( 517 | f"PermissionsBoundary must have 'Name' field in permission set {permission_set_name}", errors) 518 | 519 | # Check if its a customer managed policy 520 | if 'Path' in boundary: 521 | if not isinstance(boundary['Path'], str): 522 | log_and_append_error(f"PermissionsBoundary Path must be a string in permission set {permission_set_name}.\ 523 | The default is value for a IAM Policy path is '/', or, you can specify the path you used for your IAM Policy", 524 | errors) 525 | # else should be AWS managed policy 526 | elif 'Arn' not in boundary: 527 | log_and_append_error( 528 | f"PermissionsBoundary must have either 'Path' for customer managed policy or 'Arn' for AWS managed policy in permission set {permission_set_name}", errors) 529 | elif not is_valid_arn(boundary["Arn"]): 530 | log_and_append_error( 531 | f"Invalid ARN format for permission boundary in permission set {permission_set_name}", errors) 532 | elif not verify_policy_name_matches_arn(boundary["Name"], boundary["Arn"]): 533 | log_and_append_error( 534 | f"Permission boundary name '{boundary['Name']}' does not match ARN basename in {permission_set_name}", errors) 535 | 536 | if 'ManagedPolicies' in permission_set: 537 | policy_count = len(permission_set['ManagedPolicies']) 538 | if policy_count > 20: 539 | log_and_append_error( 540 | f"Permission set {permission_set_name} has {policy_count} managed policies. Maximum allowed is 20", errors) 541 | for policy in permission_set["ManagedPolicies"]: 542 | if not isinstance(policy, dict) or "Name" not in policy or "Arn" not in policy: 543 | log_and_append_error( 544 | f"Each managed policy must be a dictionary with 'Name' and 'Arn' fields in permission set {permission_set_name}", errors) 545 | elif not is_valid_arn(policy["Arn"]): 546 | log_and_append_error( 547 | f"Invalid ARN format for policy '{policy['Name']}' in permission set {permission_set_name}", errors) 548 | elif not verify_policy_name_matches_arn(policy["Name"], policy["Arn"]): 549 | log_and_append_error( 550 | f"Policy name '{policy['Name']}' does not match ARN basename in {permission_set_name}", errors) 551 | 552 | if 'Tags' in permission_set: 553 | for tag in permission_set["Tags"]: 554 | if not isinstance(tag, dict) or "Key" not in tag or "Value" not in tag: 555 | log_and_append_error( 556 | f"Each tag must be a dictionary with 'Key' and 'Value' fields in permission set {permission_set_name}", errors) 557 | else: 558 | if not validate_tag_key(tag["Key"]): 559 | log_and_append_error( 560 | f"Invalid tag key '{tag['Key']}' in permission set {permission_set_name}. Must be 1-128 chars and not start with aws:", errors) 561 | if not validate_tag_value(tag["Value"]): 562 | log_and_append_error( 563 | f"Invalid tag value length for key '{tag['Key']}' in permission set {permission_set_name}. Must be 0-256 chars", errors) 564 | 565 | # Validate CustomerPolicies if present 566 | if "CustomerPolicies" in permission_set: 567 | customer_policies = permission_set["CustomerPolicies"] 568 | if not isinstance(customer_policies, list): 569 | log_and_append_error( 570 | f"CustomerPolicies must be a list in permission set {permission_set_name}", errors) 571 | else: 572 | for policy in customer_policies: 573 | if not isinstance(policy, dict) or "Name" not in policy or "Path" not in policy: 574 | log_and_append_error( 575 | f"Each customer policy must be a dictionary with 'Name' and 'Path' fields in permission set {permission_set_name}", errors) 576 | 577 | if "InlinePolicies" in permission_set: 578 | inline_policies = permission_set["InlinePolicies"] 579 | if isinstance(inline_policies, list): 580 | if inline_policies: 581 | log_and_append_error( 582 | f"InlinePolicies list must be empty [] in permission set {permission_set_name}", errors) 583 | elif isinstance(inline_policies, dict): 584 | is_valid, policy_errors = validate_inline_policies(inline_policies) 585 | if not is_valid: 586 | for error in policy_errors: 587 | log_and_append_error( 588 | f"InlinePolicy error in {permission_set_name}: {error}", errors) 589 | else: 590 | log_and_append_error( 591 | f"InlinePolicies must be either a list or a dictionary in permission set {permission_set_name}", errors) 592 | 593 | 594 | def validate_permission_set_references(permission_sets: list[str], errors: list) -> None: 595 | """Validate individual permission set names in references.""" 596 | for ps_name in permission_sets: 597 | if contains_only_whitespace(ps_name): 598 | log_and_append_error( 599 | f"Permission set name '{ps_name}' cannot be only whitespace", errors) 600 | elif not ps_name: 601 | log_and_append_error("Empty permission set name found", errors) 602 | 603 | 604 | def validate_mapping_file_structure(permission_set_file, group_type, errors): 605 | logger.info( 606 | f"Validating mapping file structure for {group_type} in {permission_set_file}") 607 | 608 | def check_permission_set_name_format(items: list) -> bool: 609 | """Check if all items use the same format (string or list) for PermissionSetName""" 610 | has_string = any(isinstance(item.get('PermissionSetName'), str) 611 | for item in items) 612 | has_list = any(isinstance(item.get('PermissionSetName'), list) 613 | for item in items) 614 | return not (has_string and has_list) 615 | 616 | def validate_target_account_entry(entry, idx): 617 | """Validate a single entry in the Target/TargetAccountid list""" 618 | if isinstance(entry, str): 619 | # Direct account ID validation 620 | if not validate_account_id(entry): 621 | log_and_append_error( 622 | f"Invalid Target/TargetAccountid '{entry}' at index {idx}. Must be a valid 12-digit AWS account ID", errors) 623 | elif isinstance(entry, dict): 624 | # Validate that only allowed keys are present 625 | allowed_keys = {'OrganizationalUnits', 'Accounts'} 626 | actual_keys = set(entry.keys()) 627 | invalid_keys = actual_keys - allowed_keys 628 | if invalid_keys: 629 | log_and_append_error( 630 | f"Invalid keys {invalid_keys} found at index {idx}. Only 'OrganizationalUnits' and 'Accounts' are allowed", errors) 631 | 632 | if not actual_keys: 633 | log_and_append_error( 634 | f"Empty dictionary at index {idx}. Must contain either 'OrganizationalUnits' or 'Accounts'", errors) 635 | 636 | # Organizational units validation 637 | if 'OrganizationalUnits' in entry: 638 | if not isinstance(entry['OrganizationalUnits'], list): 639 | log_and_append_error( 640 | f"'OrganizationalUnits' must be a list at index {idx}", errors) 641 | else: 642 | for ou_path in entry['OrganizationalUnits']: 643 | if not isinstance(ou_path, str): 644 | log_and_append_error( 645 | f"OU path must be a string at index {idx}", errors) 646 | 647 | # Accounts validation 648 | if 'Accounts' in entry: 649 | if not isinstance(entry['Accounts'], list): 650 | log_and_append_error( 651 | f"'Accounts' must be a list at index {idx}", errors) 652 | else: 653 | for account in entry['Accounts']: 654 | if not isinstance(account, str): 655 | log_and_append_error( 656 | f"Account identifier must be a string at index {idx}", errors) 657 | elif account.isdigit(): 658 | if not validate_account_id(account): 659 | log_and_append_error( 660 | f"Invalid account ID '{account}' at index {idx}", errors) 661 | else: 662 | log_and_append_error( 663 | f"Invalid TargetAccountid entry type at index {idx}. Must be either a string or dictionary", errors) 664 | """ 665 | Validate the structure of the permission set mapping file. 666 | New format supporting Organizational Unit names and paths, and account names: 667 | Example of valid global mapping: 668 | [ 669 | { 670 | "GlobalGroupName": "Admin_Group", 671 | "PermissionSetName": ["example-admin"], 672 | "Target": "Global" 673 | } 674 | ] 675 | Example of valid target mapping: 676 | [ 677 | { 678 | "TargetGroupName": "Dev_Group", 679 | "PermissionSetName": ["dev-access"], 680 | "Target": [ 681 | "123456789012", 682 | { 683 | "OrganizationalUnits": [ 684 | "Production", 685 | "Development", 686 | "ProductA/Development 687 | ] 688 | }, 689 | { 690 | "Accounts": [ 691 | "123456789012", 692 | "Audit Account" 693 | ] 694 | } 695 | ] 696 | } 697 | ] 698 | 699 | Old (legacy) format for account Ids only, but now supporting Organizational Unit names and paths, and account names, with backwards compatibility: 700 | 701 | Example of valid global mapping: 702 | [ 703 | { 704 | "GlobalGroupName": "Admin_Group", 705 | "PermissionSetName": ["example-admin"], 706 | "Target": "Global" 707 | } 708 | ] 709 | Example of valid target mapping: 710 | [ 711 | { 712 | "TargetGroupName": "Dev_Group", 713 | "PermissionSetName": ["dev-access"], 714 | "Target": [ 715 | "123456789012", 716 | { 717 | "OrganizationalUnits": [ 718 | "Production", 719 | "Development", 720 | "ProductA/Development 721 | ] 722 | }, 723 | { 724 | "Accounts": [ 725 | "123456789012", 726 | "Audit Account" 727 | ] 728 | } 729 | ] 730 | } 731 | ] 732 | """ 733 | logger.info(f"Validating file structure for {group_type} mapping") 734 | 735 | required_keys = { 736 | "PermissionSetName": list 737 | } 738 | 739 | if group_type == 'global': 740 | group_key = 'GlobalGroupName' 741 | target_accountid_type = str 742 | # In global mapping, Target/TargetAccountid must be "Global" 743 | for idx, item in enumerate(permission_set_file): 744 | target_key = item.get('Target', item.get('TargetAccountid')) 745 | if target_key != 'Global': 746 | log_and_append_error( 747 | f"Global mapping must use 'Global' for Target/TargetAccountid at index {idx}", errors) 748 | elif group_type == 'target': # target 749 | group_key = 'TargetGroupName' 750 | target_accountid_type = list 751 | # In target mapping, Target/TargetAccountid must never be "Global" 752 | for idx, item in enumerate(permission_set_file): 753 | target_accountid = item.get('TargetAccountid') 754 | target = item.get('Target') 755 | if isinstance(target_accountid, list): 756 | if 'Global' in target_accountid: 757 | log_and_append_error( 758 | f"Target mapping cannot use 'Global' for Target/TargetAccountid at index {idx}", errors) 759 | elif isinstance(target, list): 760 | if 'Global' in target: 761 | log_and_append_error( 762 | f"Target mapping cannot use 'Global' for Target/TargetAccountid at index {idx}", errors) 763 | elif target_accountid == 'Global' or target == 'Global': 764 | log_and_append_error( 765 | f"Target mapping cannot use 'Global' for TargetAccountid at index {idx}", errors) 766 | 767 | # Check for consistent PermissionSetName usage 768 | if not check_permission_set_name_format(permission_set_file): 769 | log_and_append_error( 770 | f"Inconsistent format for PermissionSetName in {group_type} mapping. Use list format consistently", errors) 771 | 772 | if not isinstance(permission_set_file, list): 773 | log_and_append_error("The mapping file must be a list", errors) 774 | return 775 | 776 | for idx, permission_set in enumerate(permission_set_file): 777 | if not isinstance(permission_set, dict): 778 | log_and_append_error( 779 | f"Each item in the mapping file must be a dictionary. Item at index {idx} is not a dictionary.", errors) 780 | continue 781 | 782 | if group_key not in permission_set: 783 | log_and_append_error( 784 | f"Missing required key: '{group_key}' in mapping file at index {idx}", errors) 785 | group_name = permission_set.get(group_key) 786 | is_valid, msg = validate_whitespace(group_name, group_key) 787 | if not is_valid: 788 | log_and_append_error(f"{msg} at index {idx}", errors) 789 | 790 | if not isinstance(permission_set.get(group_key), str): 791 | log_and_append_error( 792 | f"Key '{group_key}' is not of expected type str at index {idx}", errors) 793 | 794 | for key, expected_type in required_keys.items(): 795 | if key not in permission_set: 796 | log_and_append_error( 797 | f"Missing required key: {key} in permission set at index {idx}", errors) 798 | elif not isinstance(permission_set[key], expected_type): 799 | log_and_append_error( 800 | f"Key '{key}' is not of expected type {expected_type.__name__} in permission set at index {idx}", errors) 801 | 802 | target_field = permission_set.get( 803 | 'Target', permission_set.get('TargetAccountid')) 804 | if not target_field: 805 | log_and_append_error( 806 | f"Missing required key: either 'Target' or 'TargetAccountid' in mapping at index {idx}", errors) 807 | 808 | elif not isinstance(target_field, target_accountid_type): 809 | log_and_append_error( 810 | f"'Target/TargetAccountid' must be of type {target_accountid_type.__name__} in mapping file at index {idx}", errors) 811 | 812 | elif isinstance(target_field, str): 813 | if not validate_account_id(target_field): 814 | log_and_append_error( 815 | f"Invalid Target/TargetAccountid '{target_field}' at index {idx}. Must be a list with 12 digit account Id, account name(s) in a list in a dict with 'accounts' key, and OU name/path in a list in a dict with 'OrganizationalUnits' key, for target mapping, OR 'Global' string for global mapping", errors) 816 | elif isinstance(target_field, list): 817 | for entry in target_field: 818 | validate_target_account_entry(entry, idx) 819 | 820 | if "PermissionSetName" in permission_set: 821 | if not all(isinstance(item, str) for item in permission_set["PermissionSetName"]): 822 | log_and_append_error( 823 | f"All items in 'PermissionSetName' must be strings in permission sets at index {idx}", errors) 824 | else: 825 | validate_permission_set_references( 826 | permission_set["PermissionSetName"], errors) 827 | 828 | # Handle target account validation based on mapping type 829 | if group_type == "target": 830 | # Get target field (support both Target and TargetAccountid) 831 | target_field = permission_set.get( 832 | 'Target', permission_set.get('TargetAccountid')) 833 | if target_field and isinstance(target_field, list): 834 | for entry in target_field: 835 | validate_target_account_entry(entry, idx) 836 | 837 | 838 | def validate_all_files(): 839 | """ 840 | Main function to validate all permission set and mapping files from the git repository. 841 | """ 842 | logger.info("Validating all files") 843 | errors = [] 844 | try: 845 | base_path = 'identity-center-mapping-info' 846 | param_file = 'identity-center-stacks-parameters.json' 847 | 848 | # Validate identity-center-stacks-parameters.json 849 | logger.info(f"Opening {param_file}") 850 | try: 851 | with open('identity-center-stacks-parameters.json', 'r') as file: 852 | try: 853 | parameters = json.load(file) 854 | validate_ic_stacks_parameters( 855 | parameters, errors, param_file) 856 | logger.info(f"Completed validation of {param_file}") 857 | except json.JSONDecodeError as e: 858 | log_and_append_error( 859 | f"Invalid JSON format in {param_file}: {str(e)}", errors) 860 | except (FileNotFoundError, PermissionError) as e: 861 | log_and_append_error( 862 | f"Error accessing {param_file}: {str(e)}", errors) 863 | 864 | # Validate permission set JSON files 865 | permission_sets_path = os.path.join(base_path, 'permission-sets') 866 | try: 867 | for filename in os.listdir(permission_sets_path): 868 | if filename.endswith('.json'): 869 | file_path = os.path.join(permission_sets_path, filename) 870 | logger.info(f"Opening {filename} permission set file") 871 | try: 872 | with open(file_path, 'r') as file: 873 | try: 874 | permission_set = json.load(file) 875 | if 'Name' in permission_set: 876 | if not validate_file_name_matches_content(filename, permission_set['Name']): 877 | log_and_append_error( 878 | f"File name '{filename}' does not match permission set Name '{permission_set['Name']}'", errors) 879 | validate_permission_set_schema( 880 | permission_set, errors) 881 | logger.info( 882 | f"Completed validation of permission set file: {filename}") 883 | except json.JSONDecodeError as e: 884 | log_and_append_error( 885 | f"Invalid JSON format in {filename}: {str(e)}", errors) 886 | except (FileNotFoundError, PermissionError) as e: 887 | log_and_append_error( 888 | f"Error accessing {filename}: {str(e)}", errors) 889 | except (FileNotFoundError, PermissionError) as e: 890 | log_and_append_error( 891 | f"Error accessing permission sets directory: {str(e)}", errors) 892 | 893 | # Validate global mapping file 894 | global_mapping_path = os.path.join(base_path, 'global-mapping.json') 895 | logger.info(f"Opening global-mapping.json") 896 | try: 897 | with open(global_mapping_path, 'r') as file: 898 | try: 899 | global_content = json.load(file) 900 | validate_mapping_file_structure( 901 | global_content, 'global', errors) 902 | logger.info("Completed validation of global mapping file") 903 | except json.JSONDecodeError as e: 904 | log_and_append_error( 905 | f"Invalid JSON format in global-mapping.json: {str(e)}", errors) 906 | except (FileNotFoundError, PermissionError) as e: 907 | log_and_append_error( 908 | f"Error accessing global mapping file: {str(e)}", errors) 909 | 910 | # Validate target mapping file 911 | target_mapping_path = os.path.join(base_path, 'target-mapping.json') 912 | logger.info(f"Opening target-mapping.json") 913 | try: 914 | with open(target_mapping_path, 'r') as file: 915 | try: 916 | target_content = json.load(file) 917 | validate_mapping_file_structure( 918 | target_content, 'target', errors) 919 | logger.info("Completed validation of target mapping file") 920 | except json.JSONDecodeError as e: 921 | log_and_append_error( 922 | f"Invalid JSON format in target-mapping.json: {str(e)}", errors) 923 | except (FileNotFoundError, PermissionError) as e: 924 | log_and_append_error( 925 | f"Error accessing target mapping file: {str(e)}", errors) 926 | 927 | # duplicates validation 928 | duplicate_ps_errors = validate_duplicate_ps_names(base_path) 929 | if duplicate_ps_errors: 930 | for error in duplicate_ps_errors: 931 | log_and_append_error(error, errors) 932 | 933 | if errors: 934 | error_message = "\n".join(errors) 935 | logger.info( 936 | f"Validation failed with the following errors:\n{error_message}") 937 | sys.exit(1) # Signal failure to CodeBuild 938 | else: 939 | logger.info("All validation checks completed successfully :)") 940 | return True 941 | 942 | except Exception as error: 943 | error_message = f'Exception caught: {error}' 944 | logger.error(error_message) 945 | log_and_append_error(error_message) 946 | if errors: 947 | logger.error(f'Errors during execution: {errors}') 948 | sys.exit(1) # Signal failure to CodeBuild 949 | 950 | 951 | if __name__ == "__main__": 952 | try: 953 | validate_all_files() 954 | except Exception as e: 955 | logger.error(f"Validation failed: {str(e)}") 956 | sys.exit(1) 957 | -------------------------------------------------------------------------------- /templates/identity-center-automation.template: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Description: CloudFormation template creating resources for IAM Identity Center (successor to AWS Single Sign-On) automation solution (uksb-8ggfy7rjoj). 4 | Parameters: 5 | ICInstanceARN: 6 | Type: String 7 | Description: ICInstanceARN can be found on the AWS IAM Identity Center console 'Settings' page. 8 | IdentityStoreId: 9 | Type: String 10 | Description: Identity Store ID can be found on the AWS IAM Identity Center console 'Settings' page. 11 | GlobalICGroupMappingFileName: 12 | Description: The global mapping json file name. You can use the default value. 13 | Type: String 14 | Default: global-mapping.json 15 | TargetICGroupMappingFileName: 16 | Description: The target mapping json file name. You can use the default value. 17 | Type: String 18 | Default: target-mapping.json 19 | ICMappingBucketName: 20 | Description: >- 21 | The S3 bucket that stores the source code as well as permission set and 22 | mapping definition. It's the same name that is used in 23 | identity-center-s3-bucket.template and the one you have specified in identity-center-stacks-parameters.json. 24 | Type: String 25 | ArtifactBucketName: 26 | Description: >- 27 | The S3 bucket that stores the source artifacts. We are not using artifacts for this build but need to have permissions to allow Codepipeline to execute the build. 28 | Type: String 29 | AutomationBuildProjectName: 30 | Description: "The full name of the automation CodeBuild Project" 31 | Type: String 32 | Default: ic-automation-build-project 33 | AssignmentAutomationZipFileName: 34 | Description: >- 35 | CodeBuild project code that manages IAM Identity Center account assignments. You can use the 36 | default value. 37 | Type: String 38 | Default: identity-center-auto-assign.zip 39 | AssignmentAutomationZipFileVersion: 40 | Description: >- 41 | CodeBuild project code zip version to point build to the latest version of the object. 42 | Type: String 43 | PermissionSetsAutomationZipFileName: 44 | Description: >- 45 | CodeBuild project code that manages IAM Identity Center permission sets. You can use the 46 | default value. 47 | Type: String 48 | Default: identity-center-auto-permissionsets.zip 49 | PermissionSetsAutomationZipFileVersion: 50 | Description: >- 51 | CodeBuild project code zip version to point build to the latest version of the object. 52 | Type: String 53 | SNSEmailEndpointSubscription: 54 | Description: The SNS subscription which used to receive SNS email notification. 55 | Type: String 56 | SessionDuration: 57 | Description: The length of time that the application user sessions are valid for in the ISO-8601 standard. Default is 1 hours. 58 | Type: String 59 | AllowedPattern: ^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$ 60 | Default: PT1H 61 | ICAutomationAdminArn: 62 | Type: String 63 | Description: >- 64 | The ARN of IAM Identity Center automation admin IAM role or IAM user. This IAM role(or user) 65 | will have permissions to update IAM Identity Center settings without trigger the SNS notification, 66 | besides the ICPermissionSetAssignmentAutomationRole. 67 | OrgManagementAccount: 68 | Type: String 69 | Description: Account ID of the management account. Used only to ignore the assignments for management account creaated in the management account and the solution is deployed in delegated administrator acount. 70 | AdminDelegated: 71 | Type: String 72 | AllowedValues: 73 | - "true" 74 | - "false" 75 | Default: "false" 76 | Description: Parameter to check if an AWS account is delegated as an Administrator for Identity Center 77 | ControlTowerEnabled: 78 | Type: String 79 | AllowedValues: 80 | - "true" 81 | - "false" 82 | Default: "false" 83 | Description: Parameter to check if Control Tower is deployed 84 | BuildTimeout: 85 | Type: Number 86 | Default: 120 87 | MinValue: 5 88 | MaxValue: 2160 89 | Description: The timeout in minutes for the build process between 5-2160 minutes (Up to 36 hours) 90 | 91 | Conditions: 92 | AdminDelegatedEqualsTrue: !Equals [!Ref AdminDelegated, "true"] 93 | IsICAutomationAdminArnEmpty: !Equals [!Ref ICAutomationAdminArn, ""] 94 | ControlTowerEnabledEqualsTrue: !Equals [!Ref ControlTowerEnabled, "true"] 95 | CTorAdminDelegated: 96 | !Or [ 97 | Condition: ControlTowerEnabledEqualsTrue, 98 | Condition: AdminDelegatedEqualsTrue, 99 | ] 100 | 101 | Resources: 102 | ####################################################################### 103 | # DynamoDB table to store skipped permission sets, if admin delegated # 104 | ####################################################################### 105 | SkippedPermissionSetsTable: 106 | Type: AWS::DynamoDB::Table 107 | Condition: CTorAdminDelegated 108 | Properties: 109 | SSESpecification: 110 | SSEEnabled: true 111 | AttributeDefinitions: 112 | - AttributeName: perm_set_arn 113 | AttributeType: S 114 | KeySchema: 115 | - AttributeName: perm_set_arn 116 | KeyType: HASH 117 | TableName: ic-SkippedPermissionSetsTable 118 | BillingMode: PAY_PER_REQUEST 119 | DeletionPolicy: Delete 120 | UpdateReplacePolicy: Delete 121 | ##################################################################### 122 | # Codebuild Project that manages IC permission sets and assignments # 123 | ##################################################################### 124 | ICAutomationBuildProject: 125 | Type: "AWS::CodeBuild::Project" 126 | Properties: 127 | Name: !Ref AutomationBuildProjectName 128 | Description: >- 129 | This build project runs automation to manage Permission Sets and assignments. 130 | # EncryptionKey: alias/aws/s3 131 | ServiceRole: !GetAtt ICPermissionSetAssignmentAutomationRole.Arn 132 | Artifacts: 133 | Type: NO_ARTIFACTS 134 | Environment: 135 | Type: LINUX_CONTAINER 136 | ComputeType: BUILD_GENERAL1_SMALL 137 | Image: "aws/codebuild/standard:7.0" 138 | EnvironmentVariables: 139 | - Name: S3_BUCKET_NAME 140 | Type: PLAINTEXT 141 | Value: !Join 142 | - "-" 143 | - - !Ref ICMappingBucketName 144 | - !Sub ${AWS::AccountId} 145 | - !Sub ${AWS::Region} 146 | - Name: IC_InstanceArn 147 | Type: PLAINTEXT 148 | Value: !Ref ICInstanceARN 149 | - Name: IC_S3_BucketName 150 | Type: PLAINTEXT 151 | Value: !Join 152 | - "-" 153 | - - !Ref ICMappingBucketName 154 | - !Sub ${AWS::AccountId} 155 | - !Sub ${AWS::Region} 156 | - Name: Session_Duration 157 | Type: PLAINTEXT 158 | Value: !Ref SessionDuration 159 | - Name: AWS_REGION 160 | Type: PLAINTEXT 161 | Value: !Ref "AWS::Region" 162 | - Name: Org_Management_Account 163 | Type: PLAINTEXT 164 | Value: !Ref OrgManagementAccount 165 | - Name: AdminDelegated 166 | Type: PLAINTEXT 167 | Value: !Ref AdminDelegated 168 | - Name: SkippedPermissionSetsTableName 169 | Type: PLAINTEXT 170 | Value: !If 171 | - CTorAdminDelegated 172 | - !Ref SkippedPermissionSetsTable 173 | - "" 174 | - Name: IdentityStore_Id 175 | Type: PLAINTEXT 176 | Value: !Ref IdentityStoreId 177 | - Name: GlobalFileName 178 | Type: PLAINTEXT 179 | Value: !Ref GlobalICGroupMappingFileName 180 | - Name: TargetFileName 181 | Type: PLAINTEXT 182 | Value: !Ref TargetICGroupMappingFileName 183 | - Name: PermissionSetAutomationLogGroupName 184 | Type: PLAINTEXT 185 | Value: !Ref ICPermissionsAutomationLogGroup 186 | - Name: AssignmentAutomationLogGroupName 187 | Type: PLAINTEXT 188 | Value: !Ref ICAssignmentAutomationLogGroup 189 | Source: 190 | Type: NO_SOURCE 191 | BuildSpec: !Sub |- 192 | version: "0.2" 193 | env: 194 | shell: bash 195 | variables: 196 | EXIT_CODE: 0 197 | PERM_SET_FUNCTION_EXIT_CODE: 0 198 | ASSIGNMENT_FUNCTION_EXIT_CODE: 0 199 | ERROR_MESSAGE: "" 200 | phases: 201 | pre_build: 202 | commands: 203 | - export DEBIAN_FRONTEND=noninteractive 204 | - apt update 205 | - apt-get install python3-pip -y -q 206 | - pip3 install watchtower 207 | - aws s3api get-object --bucket $S3_BUCKET_NAME --key ${PermissionSetsAutomationZipFileName} --version-id ${PermissionSetsAutomationZipFileVersion} ${PermissionSetsAutomationZipFileName} 208 | - aws s3api get-object --bucket $S3_BUCKET_NAME --key ${AssignmentAutomationZipFileName} --version-id ${AssignmentAutomationZipFileVersion} ${AssignmentAutomationZipFileName} 209 | - unzip -l ${PermissionSetsAutomationZipFileName} 210 | - unzip -l ${AssignmentAutomationZipFileName} 211 | - unzip -o ${PermissionSetsAutomationZipFileName} -d invoke_scripts 212 | - unzip -o ${AssignmentAutomationZipFileName} -d invoke_scripts 213 | build: 214 | commands: 215 | - cd invoke_scripts 216 | - set -eux 217 | - 'echo "Running the first function: auto-permissionsets.py"' 218 | - python3 auto-permissionsets.py || PERM_SET_FUNCTION_EXIT_CODE=$? 219 | - 'echo "Running the second function: auto-assignment.py"' 220 | - python3 auto-assignment.py || ASSIGNMENT_FUNCTION_EXIT_CODE=$? 221 | - | 222 | if [ $PERM_SET_FUNCTION_EXIT_CODE -ne 0 ]; then 223 | ERROR_MESSAGE="[ERROR]First function (auto-permissionsets.py) failed with exit code $PERM_SET_FUNCTION_EXIT_CODE :( Please see function logs above or in CloudWatch for details" 224 | EXIT_CODE=$PERM_SET_FUNCTION_EXIT_CODE 225 | fi 226 | if [ $ASSIGNMENT_FUNCTION_EXIT_CODE -ne 0 ]; then 227 | ERROR_MESSAGE="Second function (auto-assignment.py) failed with exit code $ASSIGNMENT_FUNCTION_EXIT_CODE :( Please see function logs above or in CloudWatch for details" 228 | EXIT_CODE=$ASSIGNMENT_FUNCTION_EXIT_CODE 229 | fi 230 | if [ $PERM_SET_FUNCTION_EXIT_CODE -ne 0 ] && [ $ASSIGNMENT_FUNCTION_EXIT_CODE -ne 0 ]; then 231 | ERROR_MESSAGE="Both functions (auto-permissionsets.py and auto-assignment.py) failed :( Please see function logs above or in CloudWatch for details" 232 | fi 233 | if [ -n "$ERROR_MESSAGE" ]; then 234 | echo "$ERROR_MESSAGE" 235 | else 236 | echo "Both functions (auto-permissionsets.py and auto-assignment.py) ran successfully. Execution is now complete :)" 237 | fi 238 | - 'echo "Exiting build with final exit code: $EXIT_CODE"' 239 | - exit $EXIT_CODE 240 | TimeoutInMinutes: !Ref BuildTimeout 241 | ICPermissionSetAssignmentAutomationRole: 242 | Type: "AWS::IAM::Role" 243 | Properties: 244 | RoleName: ICPermissionSetAssignmentAutomationRole 245 | AssumeRolePolicyDocument: 246 | Version: 2012-10-17 247 | Statement: 248 | - Effect: Allow 249 | Principal: 250 | Service: codebuild.amazonaws.com 251 | Action: 252 | - "sts:AssumeRole" 253 | Path: / 254 | Policies: 255 | - PolicyName: ICPermissionSetAutomationPolicy 256 | PolicyDocument: 257 | Statement: 258 | - Sid: SSOPermission 259 | Effect: Allow 260 | Action: 261 | - "sso:AttachCustomerManagedPolicyReferenceToPermissionSet" 262 | - "sso:AttachManagedPolicyToPermissionSet" 263 | - "sso:DescribeAccountAssignmentDeletionStatus" 264 | - "sso:DescribeAccountAssignmentCreationStatus" 265 | - "sso:CreatePermissionSet" 266 | - "sso:DeletePermissionSet" 267 | - "sso:DeletePermissionsBoundaryFromPermissionSet" 268 | - "sso:DeletePermissionsPolicy" 269 | - "sso:DescribePermissionSet" 270 | - "sso:DescribeInstance" 271 | - "sso:DescribePermissionSetProvisioningStatus" 272 | - "sso:DescribePermissionsPolicies" 273 | - "sso:DescribeRegisteredRegions" 274 | - "sso:DetachCustomerManagedPolicyReferenceFromPermissionSet" 275 | - "sso:DetachManagedPolicyFromPermissionSet" 276 | - "sso:GetInlinePolicyForPermissionSet" 277 | - "sso:GetPermissionSet" 278 | - "sso:GetPermissionsBoundaryForPermissionSet" 279 | - "sso:GetPermissionsPolicy" 280 | - "sso:ListAccountAssignments" 281 | - "sso:DeleteAccountAssignment" 282 | - "sso:DeleteInlinePolicyFromPermissionSet" 283 | - "sso:ListAccountsForProvisionedPermissionSet" 284 | - "sso:ListCustomerManagedPolicyReferencesInPermissionSet" 285 | - "sso:ListManagedPoliciesInPermissionSet" 286 | - "sso:ListAccountAssignmentDeletionStatus" 287 | - "sso:ListAccountAssignmentCreationStatus" 288 | - "sso:ListPermissionSetProvisioningStatus" 289 | - "sso:ListPermissionSets" 290 | - "sso:ListPermissionSetsProvisionedToAccount" 291 | - "sso:ListTagsForResource" 292 | - "sso:ProvisionPermissionSet" 293 | - "sso:PutInlinePolicyToPermissionSet" 294 | - "sso:PutPermissionsBoundaryToPermissionSet" 295 | - "sso:PutPermissionsPolicy" 296 | - "sso:UpdateApplicationProfileForAWSAccountInstance" 297 | - "sso:TagResource" 298 | - "sso:UntagResource" 299 | - "sso:UpdatePermissionSet" 300 | Resource: "*" 301 | - Sid: EssentialActions 302 | Effect: Allow 303 | Action: 304 | - "codepipeline:PutJobFailureResult" 305 | - "codepipeline:PutJobSuccessResult" 306 | - "logs:CreateLogDelivery" 307 | - "logs:CreateLogGroup" 308 | - "logs:CreateLogStream" 309 | - "logs:DeleteLogDelivery" 310 | - "logs:DescribeLogGroups" 311 | - "logs:DescribeLogStreams" 312 | - "logs:PutLogEvents" 313 | - "ssm:GetParameter" 314 | - "dynamodb:PutItem" 315 | - "dynamodb:DeleteItem" 316 | - "dynamodb:BatchWriteItem" 317 | - "dynamodb:Scan" 318 | Resource: "*" 319 | - Sid: S3EssentialObjectActions 320 | Effect: Allow 321 | Action: 322 | - "s3:GetObject" 323 | - "s3:GetObjectVersion" 324 | - "s3:PutObject" 325 | - "s3:PutObjectAcl" 326 | Resource: !Sub "arn:aws:s3:::${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}/*" 327 | - Sid: S3EssentialBucketAction 328 | Effect: Allow 329 | Action: 330 | - "s3:ListBucket" 331 | Resource: !Sub "arn:aws:s3:::${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}" 332 | - Sid: S3ArtifactsEssentialObjectActions 333 | Effect: Allow 334 | Action: 335 | - "s3:GetObject" 336 | Resource: !Sub "arn:aws:s3:::${ArtifactBucketName}/*" 337 | - Sid: S3ArtifactsEssentialBucketAction 338 | Effect: Allow 339 | Action: 340 | - "s3:ListBucket" 341 | Resource: !Sub "arn:aws:s3:::${ArtifactBucketName}" 342 | - Sid: KMSEssentialActions 343 | Effect: Allow 344 | Action: 345 | - "kms:Encrypt" 346 | - "kms:Decrypt" 347 | Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*" 348 | - Sid: SNSPublishAction 349 | Effect: Allow 350 | Action: 351 | - "sns:Publish" 352 | Resource: "*" 353 | - PolicyName: ICAssignmentAutomationPolicy 354 | PolicyDocument: 355 | Statement: 356 | - Sid: EssentialActions 357 | Effect: Allow 358 | Action: 359 | - "codepipeline:PutJobFailureResult" 360 | - "codepipeline:PutJobSuccessResult" 361 | - "iam:AttachRolePolicy" 362 | - "iam:DetachRolePolicy" 363 | - "iam:CreateRole" 364 | - "iam:CreateSAMLProvider" 365 | - "iam:GetRole" 366 | - "iam:GetSAMLProvider" 367 | - "iam:ListAttachedRolePolicies" 368 | - "iam:ListRolePolicies" 369 | - "iam:PutRolePolicy" 370 | - "iam:UpdateSAMLProvider" 371 | - "identitystore:ListGroups" 372 | - "identitystore:GetGroupId" 373 | - "logs:CreateLogDelivery" 374 | - "logs:CreateLogGroup" 375 | - "logs:CreateLogStream" 376 | - "logs:DeleteLogDelivery" 377 | - "logs:DescribeLogGroups" 378 | - "logs:DescribeLogStreams" 379 | - "logs:DeleteLogGroup" 380 | - "logs:PutLogEvents" 381 | - "organizations:ListAccounts" 382 | - "organizations:ListRoots" 383 | - "ssm:GetParameter" 384 | - "sso:CreateAccountAssignment" 385 | - "sso:DeleteAccountAssignment" 386 | - "sso:DescribePermissionSet" 387 | - "sso:ListAccountAssignments" 388 | - "sso:ListAccountAssignmentsForPrincipal" 389 | - "sso:ListPermissionSets" 390 | - "sso:ListTagsForResource" 391 | - "sso:UpdateSSOConfiguration" 392 | - "sso:ListAccountsForProvisionedPermissionSet" 393 | - "identitystore:DescribeGroup" 394 | Resource: "*" 395 | - Sid: OrganizationsActions 396 | Effect: Allow 397 | Action: 398 | - "organizations:ListAccountsForParent" 399 | - "organizations:DescribeAccount" 400 | - "organizations:ListChildren" 401 | - "organizations:ListOrganizationalUnitsForParent" 402 | - "organizations:ListParents" 403 | - "organizations:DescribeOrganizationalUnit" 404 | Resource: 405 | - !Sub "arn:aws:organizations::${OrgManagementAccount}:ou/o-*/ou-*" 406 | - !Sub "arn:aws:organizations::${OrgManagementAccount}:account/o-*/*" 407 | - !Sub "arn:aws:organizations::${OrgManagementAccount}:root/o-*/r-*" 408 | - Sid: S3EssentialActions 409 | Effect: Allow 410 | Action: 411 | - "s3:GetObject" 412 | - "s3:PutObject" 413 | - "s3:PutObjectAcl" 414 | - "s3:GetObjectVersion" 415 | Resource: !Sub "arn:aws:s3:::${ICMappingBucketName}-${AWS::AccountId}-${AWS::Region}/*" 416 | - Sid: KMSEssentialActions 417 | Effect: Allow 418 | Action: 419 | - "kms:Encrypt" 420 | - "kms:Decrypt" 421 | Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*" 422 | 423 | # Dedicated log group for permission set and assignment automation functions (in addition to consolidated CodeBuild Logs) 424 | ICPermissionsAutomationLogGroup: 425 | Type: "AWS::Logs::LogGroup" 426 | DeletionPolicy: Delete 427 | Properties: 428 | LogGroupName: !Sub ic-permissionsets-enabler-${AWS::AccountId}-${AWS::Region} 429 | RetentionInDays: 30 430 | ICAssignmentAutomationLogGroup: 431 | Type: "AWS::Logs::LogGroup" 432 | DeletionPolicy: Delete 433 | Properties: 434 | LogGroupName: !Sub ic-auto-assignment-enabler-${AWS::AccountId}-${AWS::Region} 435 | RetentionInDays: 30 436 | 437 | ##################################################################################################### 438 | # AWS Event Bus Policy - To receive Organization event from Management account when Admin delegated # 439 | ##################################################################################################### 440 | OrgEventsEventBus: 441 | Condition: AdminDelegatedEqualsTrue 442 | Type: AWS::Events::EventBus 443 | Properties: 444 | Name: !Sub ManagementAccountOrgEvents-${AWS::AccountId} 445 | Policy: 446 | Version: "2012-10-17" 447 | Statement: 448 | - Sid: AllowReceivingManagementAccountOrgEvents 449 | Effect: "Allow" 450 | Principal: 451 | AWS: !Sub "arn:aws:iam::${OrgManagementAccount}:root" 452 | Action: "events:PutEvents" 453 | Resource: !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/ManagementAccountOrgEvents-${AWS::AccountId}" 454 | 455 | ################################################################ 456 | # AWS Event Rules - Trigger Automation by invited account # 457 | ################################################################ 458 | ICCreateEventRuleforOrganizationInviteAccount: 459 | Type: "AWS::Events::Rule" 460 | Properties: 461 | Description: Trigger the automation CodeBuild project when an invited account successfully joins Organization 462 | EventPattern: 463 | source: 464 | - aws.organizations 465 | detail-type: 466 | - AWS API Call via CloudTrail 467 | detail: 468 | eventName: 469 | - AcceptHandshake 470 | responseElements: 471 | handshake: 472 | action: 473 | - "INVITE" 474 | state: 475 | - "ACCEPTED" 476 | Name: TriggerICAutomationJoinedInvitedAccountEnablerRule 477 | EventBusName: !If 478 | - AdminDelegatedEqualsTrue 479 | - !Ref OrgEventsEventBus 480 | - !Ref "AWS::NoValue" 481 | State: ENABLED 482 | Targets: 483 | - Arn: !GetAtt ICAutomationBuildProject.Arn 484 | RoleArn: !GetAtt AutoICEventBridgeRole.Arn 485 | Id: TargetICAutomationProject 486 | InputTransformer: 487 | InputTemplate: | 488 | { 489 | "environmentVariablesOverride": [{ 490 | "name": "EVENT_DATA", 491 | "value": "" 492 | }, 493 | { 494 | "name": "EVENT_ID", 495 | "value": "" 496 | }, 497 | { 498 | "name": "EVENT_NAME", 499 | "value": "" 500 | }, 501 | { 502 | "name": "EVENT_SOURCE", 503 | "value": "" 504 | }, 505 | { 506 | "name": "PARTY1_ID", 507 | "value": "" 508 | }, 509 | { 510 | "name": "PARTY1_TYPE", 511 | "value": "" 512 | }, 513 | { 514 | "name": "PARTY2_ID", 515 | "value": "" 516 | }, 517 | { 518 | "name": "PARTY2_TYPE", 519 | "value": "" 520 | } 521 | ] 522 | } 523 | InputPathsMap: 524 | detail_type: "$.detail-type" 525 | event_id: "$.detail.eventID" 526 | event_name: "$.detail.eventName" 527 | event_source: "$.source" 528 | party1_id: "$.detail.responseElements.handshake.parties[0].id" 529 | party1_type: "$.detail.responseElements.handshake.parties[0].type" 530 | party2_id: "$.detail.responseElements.handshake.parties[1].id" 531 | party2_type: "$.detail.responseElements.handshake.parties[1].type" 532 | ########################################################## 533 | # AWS Event Rules - Trigger Automation by new account creation success # 534 | ########################################################## 535 | ICCreateEventRuleforOrganizationCreateAccount: 536 | Type: "AWS::Events::Rule" 537 | Properties: 538 | Description: Trigger the automation CodeBuild project when new account is successfully created 539 | EventPattern: 540 | source: 541 | - aws.organizations 542 | detail-type: 543 | - AWS Service Event via CloudTrail 544 | detail: 545 | eventName: 546 | - CreateAccountResult 547 | serviceEventDetails: 548 | createAccountStatus: 549 | state: 550 | - SUCCEEDED 551 | Name: TriggerICAutomationCreateNewAccountEnablerRule 552 | EventBusName: !If 553 | - AdminDelegatedEqualsTrue 554 | - !Ref OrgEventsEventBus 555 | - !Ref "AWS::NoValue" 556 | State: ENABLED 557 | Targets: 558 | - Arn: !GetAtt ICAutomationBuildProject.Arn 559 | RoleArn: !GetAtt AutoICEventBridgeRole.Arn 560 | Id: TargetICAutomationProject 561 | InputTransformer: 562 | InputTemplate: | 563 | { 564 | "environmentVariablesOverride": [{ 565 | "name": "EVENT_DATA", 566 | "value": "" 567 | }, 568 | { 569 | "name": "EVENT_ID", 570 | "value": "" 571 | }, 572 | { 573 | "name": "EVENT_NAME", 574 | "value": "" 575 | }, 576 | { 577 | "name": "EVENT_SOURCE", 578 | "value": "" 579 | }, 580 | { 581 | "name": "EVENT_CREATE_ACCOUNT_ID", 582 | "value": "" 583 | } 584 | ] 585 | } 586 | InputPathsMap: 587 | detail_type: "$.detail-type" 588 | event_id: "$.detail.eventID" 589 | event_name: "$.detail.eventName" 590 | event_source: "$.source" 591 | account_id: "$.detail.serviceEventDetails.createAccountStatus.accountId" 592 | ########################################################## 593 | # AWS Event Rules - Trigger Automation by move account operation # 594 | ########################################################## 595 | ICCreateEventRuleforOrganizationMoveAccount: 596 | Type: "AWS::Events::Rule" 597 | Properties: 598 | Description: Trigger the automation CodeBuild project when new account is moved 599 | EventPattern: 600 | source: 601 | - aws.organizations 602 | detail-type: 603 | - AWS API Call via CloudTrail 604 | detail: 605 | eventName: 606 | - MoveAccount 607 | Name: TriggerICAutomationMoveAccountEnablerRule 608 | EventBusName: !If 609 | - AdminDelegatedEqualsTrue 610 | - !Ref OrgEventsEventBus 611 | - !Ref "AWS::NoValue" 612 | State: ENABLED 613 | Targets: 614 | - Arn: !GetAtt ICAutomationBuildProject.Arn 615 | RoleArn: !GetAtt AutoICEventBridgeRole.Arn 616 | Id: TargetICAutomationProject 617 | InputTransformer: 618 | InputTemplate: | 619 | { 620 | "environmentVariablesOverride": [{ 621 | "name": "EVENT_DATA", 622 | "value": "" 623 | }, 624 | { 625 | "name": "EVENT_ID", 626 | "value": "" 627 | }, 628 | { 629 | "name": "EVENT_NAME", 630 | "value": "" 631 | }, 632 | { 633 | "name": "EVENT_SOURCE", 634 | "value": "" 635 | }, 636 | { 637 | "name": "EVENT_MOVED_ACCOUNT_ID", 638 | "value": "" 639 | } 640 | ] 641 | } 642 | InputPathsMap: 643 | detail_type: "$.detail-type" 644 | event_id: "$.detail.eventID" 645 | event_name: "$.detail.eventName" 646 | event_source: "$.source" 647 | account_id: "$.detail.requestParameters.accountId" 648 | 649 | ########################################################## 650 | # AWS Event Rules - Trigger Automation by new account creation success # 651 | ########################################################## 652 | ICCreateEventRuleforOrganizationCreateOU: 653 | Type: "AWS::Events::Rule" 654 | Properties: 655 | Description: Trigger the automation CodeBuild project when new OU is created 656 | EventPattern: 657 | source: 658 | - aws.organizations 659 | detail-type: 660 | - AWS API Call via CloudTrail 661 | detail: 662 | eventName: 663 | - CreateOrganizationalUnit 664 | responseElements: 665 | organizationalUnit: 666 | id: 667 | - { "prefix": "ou" } 668 | Name: TriggerICAutomationCreateNewOUEnablerRule 669 | EventBusName: !If 670 | - AdminDelegatedEqualsTrue 671 | - !Ref OrgEventsEventBus 672 | - !Ref "AWS::NoValue" 673 | State: ENABLED 674 | Targets: 675 | - Arn: !GetAtt ICAutomationBuildProject.Arn 676 | RoleArn: !GetAtt AutoICEventBridgeRole.Arn 677 | Id: TargetICAutomationProject 678 | InputTransformer: 679 | InputTemplate: | 680 | { 681 | "environmentVariablesOverride": [{ 682 | "name": "EVENT_DATA", 683 | "value": "" 684 | }, 685 | { 686 | "name": "EVENT_ID", 687 | "value": "" 688 | }, 689 | { 690 | "name": "EVENT_NAME", 691 | "value": "" 692 | }, 693 | { 694 | "name": "EVENT_SOURCE", 695 | "value": "" 696 | }, 697 | { 698 | "name": "EVENT_CREATE_OU_ID", 699 | "value": "" 700 | }, 701 | { 702 | "name": "EVENT_CREATE_OU_NAME", 703 | "value": "" 704 | } 705 | ] 706 | } 707 | InputPathsMap: 708 | detail_type: "$.detail-type" 709 | event_id: "$.detail.eventID" 710 | event_name: "$.detail.eventName" 711 | event_source: "$.source" 712 | ou_id: "$.detail.responseElements.organizationalUnit.id" 713 | ou_name: "$.detail.responseElements.organizationalUnit.name" 714 | 715 | ########################################################################################################### 716 | # AWS Event Rules - Detect manual user interaction with the IAM Identity Center - comment line 258-366 to disable# 717 | ############################################################################################################ 718 | ICManualActionDetectionRule1: 719 | Type: "AWS::Events::Rule" 720 | Properties: 721 | Description: Trigger the automation CodeBuild project when it detects manual user interaction from eventSource sso.amazonaws.com 722 | EventPattern: 723 | source: 724 | - aws.sso 725 | detail-type: 726 | - AWS API Call via CloudTrail 727 | detail: 728 | eventSource: 729 | - sso.amazonaws.com 730 | eventName: 731 | - AssociateProfile 732 | - AttachManagedPolicyToPermissionSet 733 | - AttachCustomerManagedPolicyReferenceToPermissionSet 734 | - DeletePermissionsBoundaryFromPermissionSet 735 | - DetachCustomerManagedPolicyReferenceFromPermissionSet 736 | - PutPermissionsBoundaryToPermissionSet 737 | - CreateAccountAssignment 738 | - CreateInstanceAccessControlAttributeConfiguration 739 | - CreatePermissionSet 740 | - CreateProfile 741 | - DeleteAccountAssignment 742 | - DeleteInlinePolicyFromPermissionSet 743 | - DeleteInstanceAccessControlAttributeConfiguration 744 | - UpdateInstanceAccessControlAttributeConfiguration 745 | - DeletePermissionSet 746 | - DeletePermissionsPolicy 747 | - DetachManagedPolicyFromPermissionSet 748 | - ProvisionPermissionSet 749 | - PutInlinePolicyToPermissionSet 750 | - PutPermissionsPolicy 751 | - TagResource 752 | - UntagResource 753 | - UpdatePermissionSet 754 | - UpdateProfile 755 | - UpdateInstance 756 | - UpdateSSOConfiguration 757 | userIdentity: 758 | sessionContext: 759 | sessionIssuer: 760 | userName: 761 | - anything-but: 762 | - !If 763 | - IsICAutomationAdminArnEmpty 764 | - !ImportValue ICAdminRoleArn 765 | - !Ref ICAutomationAdminArn 766 | - ICPermissionSetAssignmentAutomationRole 767 | Name: ICManualActionDetectionRule1 768 | State: ENABLED 769 | Targets: 770 | - Arn: !GetAtt ICAutomationBuildProject.Arn 771 | RoleArn: !GetAtt AutoICEventBridgeRole.Arn 772 | Id: TargetICAutomationProject 773 | InputTransformer: 774 | InputTemplate: | 775 | { 776 | "environmentVariablesOverride": [ 777 | { 778 | "name": "EVENT_DATA", 779 | "value": "" 780 | }, 781 | { 782 | "name": "EVENT_ID", 783 | "value": "" 784 | }, 785 | { 786 | "name": "EVENT_NAME", 787 | "value": "" 788 | }, 789 | { 790 | "name": "EVENT_SOURCE", 791 | "value": "" 792 | }, 793 | { 794 | "name": "USER_IDENTITY", 795 | "value": "" 796 | } 797 | ] 798 | } 799 | InputPathsMap: 800 | detail_type: "$.detail-type" 801 | event_id: "$.detail.eventID" 802 | event_name: "$.detail.eventName" 803 | event_source: "$.source" 804 | user_identity: "$.detail.userIdentity.arn" 805 | - Arn: !GetAtt ICAlertSNSNotificationLambda.Arn 806 | Id: ManualChangeAlert 807 | 808 | ICManualActionDetectionRule2: 809 | Type: "AWS::Events::Rule" 810 | Properties: 811 | Description: Trigger the automation CodeBuild project when it detects manual user interaction from eventSource sso-directory.amazonaws.com 812 | EventPattern: 813 | source: 814 | - aws.sso-directory 815 | detail-type: 816 | - AWS API Call via CloudTrail 817 | detail: 818 | eventSource: 819 | - sso-directory.amazonaws.com 820 | eventName: 821 | - AddMemberToGroup 822 | - CompleteVirtualMfaDeviceRegistration 823 | - CompleteWebAuthnDeviceRegistration 824 | - CreateAlias 825 | - CreateBearerToken 826 | - CreateExternalIdPConfigurationForDirectory 827 | - CreateGroup 828 | - CreateProvisioningTenant 829 | - CreateUser 830 | - DeleteBearerToken 831 | - DeleteExternalIdPCertificate 832 | - DeleteExternalIdPConfigurationForDirectory 833 | - DeleteGroup 834 | - DeleteMfaDeviceForUser 835 | - DeleteProvisioningTenant 836 | - DeleteUser 837 | - DisableExternalIdPConfigurationForDirectory 838 | - DisableUser 839 | - EnableExternalIdPConfigurationForDirectory 840 | - EnableUser 841 | - ImportExternalIdPCertificate 842 | - RemoveMemberFromGroup 843 | - StartVirtualMfaDeviceRegistration 844 | - StartWebAuthnDeviceRegistration 845 | - UpdateExternalIdPConfigurationForDirectory 846 | - UpdateGroup 847 | - UpdateGroupDisplayName 848 | - UpdateMfaDeviceForUser 849 | - UpdatePassword 850 | - UpdateUser 851 | - UpdateUserName 852 | - VerifyEmail 853 | userIdentity: 854 | sessionContext: 855 | sessionIssuer: 856 | userName: 857 | - anything-but: 858 | - !If 859 | - IsICAutomationAdminArnEmpty 860 | - !ImportValue ICAdminRoleArn 861 | - !Ref ICAutomationAdminArn 862 | - ICPermissionSetAssignmentAutomationRole 863 | Name: ICManualActionDetectionRule2 864 | State: ENABLED 865 | Targets: 866 | - Arn: !GetAtt ICAutomationBuildProject.Arn 867 | RoleArn: !GetAtt AutoICEventBridgeRole.Arn 868 | Id: TargetICAutomationProject 869 | InputTransformer: 870 | InputTemplate: | 871 | { 872 | "environmentVariablesOverride": [ 873 | { 874 | "name": "EVENT_DATA", 875 | "value": "" 876 | }, 877 | { 878 | "name": "EVENT_ID", 879 | "value": "" 880 | }, 881 | { 882 | "name": "EVENT_NAME", 883 | "value": "" 884 | }, 885 | { 886 | "name": "EVENT_SOURCE", 887 | "value": "" 888 | }, 889 | { 890 | "name": "USER_IDENTITY", 891 | "value": "" 892 | } 893 | ] 894 | } 895 | InputPathsMap: 896 | detail_type: "$.detail-type" 897 | event_id: "$.detail.eventID" 898 | event_name: "$.detail.eventName" 899 | event_source: "$.source" 900 | user_identity: "$.detail.userIdentity.arn" 901 | 902 | AutoICEventBridgeRole: 903 | Type: AWS::IAM::Role 904 | Properties: 905 | RoleName: AutoICEventBridgeRole 906 | AssumeRolePolicyDocument: 907 | Version: "2012-10-17" 908 | Statement: 909 | - Effect: "Allow" 910 | Principal: 911 | Service: "events.amazonaws.com" 912 | Action: "sts:AssumeRole" 913 | Policies: 914 | - PolicyName: "InvokeCodeBuild" 915 | PolicyDocument: 916 | Version: "2012-10-17" 917 | Statement: 918 | - Effect: "Allow" 919 | Action: "codebuild:StartBuild" 920 | Resource: !GetAtt ICAutomationBuildProject.Arn 921 | 922 | ICScheduledRuleBaselining: 923 | Type: AWS::Events::Rule 924 | Properties: 925 | Description: Schedule CloudWatch event rule (re-baselining) for the CodeBuild project (every 12 hour by default) 926 | ScheduleExpression: rate(12 hours) 927 | State: "ENABLED" 928 | Targets: 929 | - Arn: !GetAtt ICAutomationBuildProject.Arn 930 | RoleArn: !GetAtt AutoICEventBridgeRole.Arn 931 | Id: TargetICAutomationProject 932 | InputTransformer: 933 | InputTemplate: | 934 | { 935 | "environmentVariablesOverride": [ 936 | { 937 | "name": "EVENT_DATA", 938 | "value": "" 939 | }, 940 | { 941 | "name": "EVENT_SOURCE", 942 | "value": "" 943 | } 944 | ] 945 | } 946 | InputPathsMap: 947 | detail_type: "$.detail-type" 948 | event_source: "$.source" 949 | 950 | SNSICManualActionAlertTopic: 951 | Type: AWS::SNS::Topic 952 | Properties: 953 | DisplayName: IC-Manual-Modification-Detection-Alert 954 | TopicName: IC-Manual-Modification-Detection-Alert 955 | Subscription: 956 | - Endpoint: !Ref SNSEmailEndpointSubscription 957 | Protocol: email 958 | 959 | ##################################################### 960 | ## Lambda function(3) to customize SNS Email Subject# 961 | ##################################################### 962 | ICAlertSNSNotificationLambda: 963 | Type: "AWS::Lambda::Function" 964 | Properties: 965 | FunctionName: ic-alert-SNSnotification 966 | Handler: index.lambda_handler 967 | Role: !GetAtt ICAlertSNSNotificationRole.Arn 968 | Code: 969 | ZipFile: | 970 | import boto3 971 | import os 972 | import json 973 | sns_client = boto3.client("sns") 974 | sns_arn= os.environ.get("SNSTopic_ARN") 975 | sns_email_subject="AWS IAM Identity Center Manual Modification" 976 | 977 | def lambda_handler(event, context): 978 | print(event) 979 | try: 980 | if event['detail']['eventName']: 981 | resp = sns_client.publish(TargetArn=sns_arn, Message="The following manual change was detected and will be reverted:"+"\n\n"+json.dumps(event, indent=4, sort_keys=False).replace('"',''), Subject=sns_email_subject+" - "+event['detail']['eventName']) 982 | else: 983 | resp = sns_client.publish(TargetArn=sns_arn, Message="The following manual change was detected and will be reverted:"+"\n\n"+json.dumps(event, indent=4, sort_keys=False).replace('"',''), Subject=sns_email_subject+" - Unknown") 984 | print(resp) 985 | print("Info.Execution completed correctly.") 986 | except Exception as ex: 987 | print(ex) 988 | print("Error.Execution completed incorrectly") 989 | return {'statusCode': 200,'body': json.dumps('successfully executed lambda')} 990 | Runtime: "python3.12" 991 | Environment: 992 | Variables: 993 | SNSTopic_ARN: !Ref SNSICManualActionAlertTopic 994 | MemorySize: 128 995 | Timeout: 120 996 | 997 | ICAlertSNSNotificationRole: 998 | Type: "AWS::IAM::Role" 999 | Properties: 1000 | RoleName: ICAlertSNSNotificationRole 1001 | AssumeRolePolicyDocument: 1002 | Version: 2012-10-17 1003 | Statement: 1004 | - Effect: Allow 1005 | Principal: 1006 | Service: lambda.amazonaws.com 1007 | Action: 1008 | - "sts:AssumeRole" 1009 | Path: / 1010 | Policies: 1011 | - PolicyName: ICAssignmentAutomationPolicy 1012 | PolicyDocument: 1013 | Statement: 1014 | - Sid: EssentialActions 1015 | Effect: Allow 1016 | Action: 1017 | - "sns:Publish" 1018 | Resource: !Ref SNSICManualActionAlertTopic 1019 | - Sid: CloudWatchLog 1020 | Effect: Allow 1021 | Action: 1022 | - "logs:CreateLogDelivery" 1023 | - "logs:CreateLogGroup" 1024 | - "logs:CreateLogStream" 1025 | - "logs:DeleteLogDelivery" 1026 | - "logs:DescribeLogGroups" 1027 | - "logs:DescribeLogStreams" 1028 | - "logs:PutLogEvents" 1029 | Resource: "*" 1030 | --------------------------------------------------------------------------------