├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── code ├── __init__.py ├── configuration │ ├── resource_types_actions_allowed.json │ └── resource_types_supported.json ├── helpers │ ├── __init__.py │ ├── controllers │ │ ├── __init__.py │ │ ├── policies.py │ │ ├── policy.py │ │ ├── role.py │ │ ├── roles.py │ │ └── table.py │ ├── custom_resource_handler.py │ ├── defaults.py │ ├── exceptions.py │ ├── logger.py │ ├── partition.py │ ├── resources │ │ ├── __init__.py │ │ ├── base.py │ │ ├── ec2_instance.py │ │ ├── factory.py │ │ ├── lambda_function.py │ │ └── s3_bucket.py │ ├── resources_generator.py │ ├── stack.py │ └── utils │ │ ├── __init__.py │ │ ├── catch_error.py │ │ ├── convertor.py │ │ ├── rand.py │ │ └── session.py ├── partition_phase_a.py └── partition_phase_b.py ├── demo ├── demo.md ├── demo.sh ├── launch_productsA.yml ├── launch_productsB.yml ├── portfolios.yml └── products │ ├── ec2 │ └── template.yml │ ├── ec2_with_profile │ └── template.yml │ ├── lambda │ └── template.yml │ └── s3 │ └── template.yml ├── deployment ├── __init__.py └── template.yml └── docs ├── custom_resource.md ├── images ├── architecture.png ├── concept.png ├── flow.png ├── policy_and_role_as_boundary.png └── usecase-example.png └── support_new_resource_type.md /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | venv*/ 4 | *.zip 5 | .idea/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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](https://github.com/aws-samples/aws-service-catalog-portfolio-boundary/issues), or [recently closed](https://github.com/aws-samples/aws-service-catalog-portfolio-boundary/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), 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 *master* 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'](https://github.com/aws-samples/aws-service-catalog-portfolio-boundary/labels/help%20wanted) 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](https://github.com/aws-samples/aws-service-catalog-portfolio-boundary/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 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 this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create a security partition for your applications using AWS Service Catalog and AWS Lambda 2 | The repository includes python3 AWS Lambda function code and a CFN template to deploy the security partition solution. 3 | The AWS Lambda function automates the application infrastructure separation for each application that is provisioned via AWS Service Catalog portfolio. 4 | Application related resources will be able to communicate using AWS API calls, between themselves, while being banned to 5 | issue AWS API calls to resources out of the application scope. 6 | To learn more you can read [the blog post](https://aws.amazon.com/blogs/mt/create-a-security-partition-for-your-applications-using-aws-service-catalog-and-aws-lambda/). 7 | 8 | ![Security Partition Solution Flow](./docs/images/concept.png) 9 | 10 | ## Architecture and Flow 11 | 12 | ![Security Partition Architecture](./docs/images/architecture.png) 13 | 14 | As a service catalog product is launched, CloudFormation stack is created based on the product’s CloudFormation template. 15 | Once all the resources are provisioned a final custom resource will invoke the “phase a” lambda function to execute the logic of the security partition. 16 | Phase A lambda inquires for the provisioned resources and their provisioning status. 17 | For each resource it calls “assume role” with the portfolio role. If the resource already assumes a role, it attaches the portfolio policy to that role. 18 | Finally, it updates the DynamoDB table with new items which includes portfolio id and resource arn. Once it ends, the lambda sends response to CloudFormation in order to finalize the provisioning process. 19 | When the DynamoDB table is updated, DynamoDB stream invokes the “phase b” lambda function. 20 | The latter creates new Portfolio Policy version, according to the current information in the table, and set it to be the default version. 21 | 22 | ![Security Partition Solution Flow](./docs/images/flow.png) 23 | 24 | Currently, the solution supports a limited set of resource types: 25 | - "AWS::EC2::Instance" 26 | - "AWS::Lambda::Function" 27 | - "AWS::S3::Bucket" 28 | 29 | See instructions for adding support for other resource types [Adding support for new resource types](docs/support_new_resource_type.md) 30 | 31 | ## Deploy 32 | Use the CloudFormation template to deploy the required AWS resources: 33 | - AWS DynamoDB table 34 | - AWS Lambda Function triggered by DynamoDB Stream 35 | - AWS Lambda Function triggered by CFN custom resource 36 | 37 | 1. Clone aws-service-catalog-portfolio-partition repository 38 | 1. Package the lambda functions code ([see documentation for creating lambda package](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html)) and upload the resulted zip file to a S3 bucket. 39 | ```bash 40 | cd aws-service-catalog-portfolio-partition/code/ 41 | zip -r ../code.zip . 42 | aws s3 cp ../code.zip s3:/// 43 | ``` 44 | 1. Use aws cli to create stack with the CloudFormation template. 45 | Find the CloudFormation template, template.yml, under the deployment folder. 46 | When creating the stack, supply the S3 bucket name and S3 key name of the Lambda functions package. 47 | ```bash 48 | cd aws-service-catalog-portfolio-partition/deployment/ 49 | aws cloudformation create-stack --stack-name sc-security-partition --template-body file://template.yml --parameters ParameterKey=LambdaCodeS3Bucket,ParameterValue= ParameterKey=LambdaCodeS3Key,ParameterValue= --capabilities CAPABILITY_IAM 50 | ``` 51 | This template will provision 2 Lambda Functions and a DynamoDB Table. The ARN of the “phase a” lambda function will be exported to be consumed by the SetSecurityPartition custom resource. 52 | ```yaml 53 | AWSTemplateFormatVersion: '2010-09-09' 54 | 55 | Description: Create all prerequisites for the portfolio security partition solution. 56 | 57 | Parameters: 58 | LambdaCodeS3Bucket: 59 | Description: S3 Bucket Name in which the lambda package stored 60 | Type: String 61 | LambdaCodeS3Key: 62 | Description: S3 Bucket Key of the stored lambda package 63 | Type: String 64 | 65 | Outputs: 66 | PartitionPhaseAFunctionArn: 67 | Description: Lambda function to serve the custom resource of portfolio as security partition 68 | Value: !GetAtt PartitionPhaseAFunction.Arn 69 | Export: 70 | Name: PartitionPhaseAFunctionArn 71 | 72 | Resources: 73 | PartitionPhaseAFunction: 74 | Type: 'AWS::Lambda::Function' 75 | Properties: 76 | Description: 'The first of two lambda functions that applies a security partition 77 | around resources which have originated in the same Service Catalog Portfolio' 78 | LambdaPhaseARole: 79 | Type: 'AWS::IAM::Role' 80 | PartitionPhaseBFunction: 81 | Type: 'AWS::Lambda::Function' 82 | Properties: 83 | Description: 'The second of two lambda functions that applies a security partition 84 | around resources which have originated in the same Service Catalog Portfolio' 85 | LambdaPhaseBRole: 86 | Type: 'AWS::IAM::Role' 87 | PolicyTable: 88 | Type: 'AWS::DynamoDB::Table' 89 | PolicyTableEventStream: 90 | Type: "AWS::Lambda::EventSourceMapping" 91 | EventSourceArn: !GetAtt PolicyTable.StreamArn 92 | FunctionName: !GetAtt PartitionPhaseBFunction.Arn 93 | ``` 94 | ## Portfolios and Products Onboarding 95 | To apply the security partition on a new created portfolio, you will have to revise your products to include the SetSecurityPartition custom resource. 96 | For each product associated with the portfolio, its CloudFormation template should be modified to include the custom resource in the following format: 97 | ```yaml 98 | SetSecurityPartition: 99 | Type: Custom::SetSecurityPartition 100 | DependsOn: 101 | Properties: 102 | ServiceToken: !ImportValue PartitionPhaseAFunctionArn 103 | ``` 104 | Note that the **DependsOn** attribute should include the logical name of the resources, such that the custom resource will be the last to be provisioned. 105 | ## Customize the Security Partition 106 | For additional control when you define the security partition, the permitted actions per resource type, within the Portfolio Policy, are configurable. For that we use configuration file which is located under Aws-service-catalog-portfolio-partition/code/configuration/resource_types_actions_allowed.json 107 | Allowed actions per resource type: 108 | ```json 109 | { 110 | "AWS::EC2::Instance": [ 111 | "ec2:StopInstances", 112 | "ec2:StartInstances" 113 | ], 114 | "AWS::Lambda::Function": [ 115 | "lambda:*" 116 | ], 117 | "AWS::S3::Bucket": [ 118 | "s3:*" 119 | ] 120 | } 121 | ``` 122 | For a resource type not listed, the default value will be "\*". 123 | 124 | ## Supported Resource Types 125 | When it comes to IAM, each AWS resource type might have a different behavior. Moreover, there are resource types which are actively accessing other resources and thus might assume roles (e.g., ec2 instance, lambda function) while others are more passive (e.g., s3, dynamoDB). To handle the different resources, the portfolio security partition implementation maintain a whitelist of supported resource types. In addition, to be supported, each resource type must implement the resources/base.py interface. 126 | Please find detailed instructions [here](./docs/support_new_resource_type.md) in the git repository documentation. 127 | Once a new resource type is whitelisted and has an implemented object, the lambda functions permissions must be updated to be allowed with the new required actions. Those actions may be concluded from the api calls been used within the new implemented object. 128 | Following are the current supported resource types: 129 | ```json 130 | "AWS::EC2::Instance", 131 | "AWS::Lambda::Function", 132 | "AWS::S3::Bucket" 133 | ``` 134 | ## License Summary 135 | This sample code is made available under a modified MIT license. See the LICENSE file. 136 | -------------------------------------------------------------------------------- /code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/code/__init__.py -------------------------------------------------------------------------------- /code/configuration/resource_types_actions_allowed.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWS::EC2::Instance": [ 3 | "ec2:StopInstances", 4 | "ec2:StartInstances" 5 | ], 6 | "AWS::Lambda::Function": [ 7 | "lambda:*" 8 | ], 9 | "AWS::S3::Bucket": [ 10 | "s3:*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /code/configuration/resource_types_supported.json: -------------------------------------------------------------------------------- 1 | { 2 | "Applicable": [ 3 | "AWS::EC2::Instance", 4 | "AWS::Lambda::Function", 5 | "AWS::DynamoDB::Table", 6 | "AWS::S3::Bucket" 7 | ], 8 | "NotApplicable": [ 9 | "Custom::.*", 10 | "AWS::IAM::.*", 11 | "AWS::CloudFormation::.*", 12 | "AWS::Lambda::EventSourceMapping", 13 | "AWS::EC2::SecurityGroup", 14 | "AWS::Redshift::ClusterSubnetGroup", 15 | "AWS::Redshift::Cluster" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /code/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/code/helpers/__init__.py -------------------------------------------------------------------------------- /code/helpers/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/code/helpers/controllers/__init__.py -------------------------------------------------------------------------------- /code/helpers/controllers/policies.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import jmespath 18 | from botocore.exceptions import ClientError 19 | from helpers.logger import logger 20 | from helpers.utils.session import Session 21 | 22 | 23 | class Policies(object): 24 | 25 | def __init__(self): 26 | self.session = Session() 27 | self.iam_client = self.session.client('iam') 28 | self.iam_resource = self.session.resource('iam') 29 | 30 | def find(self, path, name): 31 | paginator = self.iam_client.get_paginator('list_policies') 32 | response_iterator = paginator.paginate( 33 | Scope='Local', 34 | OnlyAttached=False, 35 | PathPrefix='/{}/'.format(path), 36 | PaginationConfig={ 37 | 'MaxItems': 123, 38 | 'PageSize': 123 39 | } 40 | ) 41 | result = [] 42 | for response in response_iterator: 43 | result += jmespath.search( 44 | "Policies[?PolicyName=='{}'].Arn".format(name), 45 | response 46 | ) 47 | return jmespath.search("[0]", result) 48 | 49 | def create(self, path, name, base_policy): 50 | try: 51 | response = self.iam_client.create_policy( 52 | Path='/{}/'.format(path), 53 | PolicyName=name, 54 | PolicyDocument=base_policy, 55 | Description='Set the access to resources provisioned by products \ 56 | associated with Service Catalog Portfolio {}'.format(name) 57 | ) 58 | logger.info('Policy created: {}'.format(name)) 59 | return response['Policy']['Arn'] 60 | except ClientError as e: 61 | if e.response['Error']['Code'] == 'EntityAlreadyExists': 62 | return self.find(path, name) 63 | -------------------------------------------------------------------------------- /code/helpers/controllers/policy.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import json 18 | 19 | from helpers.logger import logger 20 | from helpers.utils.session import Session 21 | from helpers.utils.catch_error import client_error_retry 22 | from helpers.utils.convertor import arn_to_name 23 | 24 | ''' 25 | Limits: 26 | Managed policies attached to an IAM role 10 27 | Roles in an instance profile 1 28 | Versions of a managed policy that can be stored 5 29 | Role trust policy JSON text 2,048 characters 30 | Role inline policy size cannot exceed 10,240 characters 31 | (exclude whitespaces) 32 | The size of each managed policy cannot exceed 6,144 characters 33 | (exclude whitespaces) 34 | ''' 35 | 36 | 37 | class Policy(object): 38 | 39 | def __init__(self, arn): 40 | self.session = Session() 41 | self.iam_client = self.session.client('iam') 42 | self.iam_resource = self.session.resource('iam') 43 | self.arn = arn 44 | self.name = arn_to_name(self.arn) 45 | self.aro = self.iam_resource.Policy(self.arn) 46 | 47 | def delete_non_default_versions(self): 48 | for policy_version in self.aro.versions.all(): 49 | if not policy_version.is_default_version: 50 | policy_version.delete() 51 | 52 | def detach_roles(self): 53 | for rol in self.aro.attached_roles.all(): 54 | rol.detach_policy(PolicyArn=self.arn) 55 | 56 | @client_error_retry('LimitExceeded', delete_non_default_versions) 57 | def update_document(self, document): 58 | self.iam_client.create_policy_version( 59 | PolicyArn=self.arn, 60 | PolicyDocument=json.dumps(document), 61 | SetAsDefault=True 62 | ) 63 | logger.info('Policy document updated: {}'.format(document)) 64 | 65 | @client_error_retry('DeleteConflict', detach_roles) 66 | @client_error_retry('DeleteConflict', delete_non_default_versions) 67 | def delete(self): 68 | self.aro.delete() 69 | -------------------------------------------------------------------------------- /code/helpers/controllers/role.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import json 18 | import jmespath 19 | from helpers.logger import logger 20 | from helpers.utils.session import Session 21 | from helpers.utils.catch_error import client_error_retry 22 | from helpers.utils.convertor import arn_to_name 23 | 24 | ''' 25 | Limits: 26 | Managed policies attached to an IAM role 10 27 | Roles in an instance profile 1 28 | Versions of a managed policy that can be stored 5 29 | Role trust policy JSON text 2,048 characters 30 | Role inline policy size cannot exceed 10,240 characters 31 | (exclude whitespaces) 32 | The size of each managed policy cannot exceed 6,144 characters 33 | (exclude whitespaces) 34 | ''' 35 | 36 | 37 | class Role(object): 38 | 39 | def __init__(self, arn): 40 | self.session = Session() 41 | self.iam_client = self.session.client('iam') 42 | self.iam_resource = self.session.resource('iam') 43 | self.arn = arn 44 | self.name = arn_to_name(self.arn) 45 | self.aro = self.iam_resource.Role(self.name) 46 | 47 | def attach_policy(self, policy_arn): 48 | self.aro.attach_policy(PolicyArn=policy_arn) 49 | 50 | def update_trust_document(self, service_name): 51 | policy = self.aro.AssumeRolePolicy() 52 | policy_doc = self.aro.assume_role_policy_document 53 | principal_service = jmespath.search( 54 | "Statement[].Principal.Service", policy_doc 55 | ) 56 | principal_service = jmespath.search("[][][]", principal_service) 57 | if service_name not in principal_service: 58 | principal_service.append(service_name) 59 | policy_doc['Statement'] = [ 60 | {'Action': 'sts:AssumeRole', 61 | 'Effect': 'Allow', 62 | 'Principal': {'Service': principal_service}} 63 | ] 64 | policy.update(PolicyDocument=json.dumps(policy_doc)) 65 | logger.debug("Trust policy updated: {}".format(policy_doc)) 66 | 67 | def _stop_being_assumed(self): 68 | pass 69 | 70 | def _detach_policy(self): 71 | pass 72 | 73 | # TODO implement the resolver methods 74 | @client_error_retry('DeleteConflict', _stop_being_assumed) 75 | @client_error_retry('DeleteConflict', _detach_policy) 76 | def delete(self): 77 | self.aro.delete() 78 | -------------------------------------------------------------------------------- /code/helpers/controllers/roles.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from botocore.exceptions import ClientError 18 | from helpers.logger import logger 19 | from helpers.utils.session import Session 20 | from helpers.utils.catch_error import client_error_ignore 21 | 22 | 23 | class Roles(object): 24 | 25 | def __init__(self): 26 | self.session = Session() 27 | self.iam_client = self.session.client('iam') 28 | self.iam_resource = self.session.resource('iam') 29 | 30 | @client_error_ignore('NoSuchEntity') 31 | def find(self, name): 32 | aro = self.iam_resource.Role(name) 33 | aro.create_date 34 | return aro.arn 35 | 36 | def create(self, path, name, base_trust_policy): 37 | try: 38 | response = self.iam_client.create_role( 39 | Path='/{}/'.format(path), 40 | RoleName=name, 41 | AssumeRolePolicyDocument=base_trust_policy, 42 | Description='Assumed by resources provisioned by products \ 43 | associated with Service Catalog Portfolio {}'.format(name) 44 | ) 45 | logger.debug('Role created: {}'.format(name)) 46 | return response['Role']['Arn'] 47 | except ClientError as err: 48 | if err.response['Error']['Code'] == 'EntityAlreadyExists': 49 | return self.find(name) 50 | else: 51 | raise err 52 | -------------------------------------------------------------------------------- /code/helpers/controllers/table.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from helpers.utils.session import Session 18 | 19 | 20 | class Table(): 21 | def __init__(self, name): 22 | self.session = Session() 23 | self.dynamodb_client = self.session.client('dynamodb') 24 | self.dynamodb_resource = self.session.resource('dynamodb') 25 | self.name = name 26 | self.aro = self.dynamodb_resource.Table(self.name) 27 | 28 | def add(self, items): 29 | with self.aro.batch_writer() as batch: 30 | for item in items: 31 | batch.put_item(Item=item) 32 | 33 | def remove(self, items): 34 | with self.aro.batch_writer() as batch: 35 | for item in items: 36 | print('item: {}'.format(item)) 37 | batch.delete_item(Key=item) 38 | -------------------------------------------------------------------------------- /code/helpers/custom_resource_handler.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import json 18 | from botocore.vendored import requests 19 | from helpers.logger import logger 20 | 21 | SUCCESS = 'SUCCESS' 22 | FAILED = 'FAILED' 23 | 24 | 25 | def wrap_user_handler(func, base_response=None): 26 | def wrapper_func(event, context): 27 | 28 | logger.debug( 29 | "Received %s request with event: %s" % ( 30 | event['RequestType'], json.dumps(event) 31 | ) 32 | ) 33 | 34 | response = { 35 | "StackId": event["StackId"], 36 | "RequestId": event["RequestId"], 37 | "LogicalResourceId": event["LogicalResourceId"], 38 | "PhysicalResourceId": "custom_resource_physical_id", 39 | "Status": SUCCESS, 40 | } 41 | if event.get("PhysicalResourceId", False): 42 | response["PhysicalResourceId"] = event["PhysicalResourceId"] 43 | 44 | if base_response is not None: 45 | response.update(base_response) 46 | 47 | try: 48 | response.update(func(event, context)) 49 | except NoResponse: 50 | # Do nothing, maybe we're being rescheduled? 51 | return 52 | except Exception as e: 53 | logger.exception("Failed to execute resource function") 54 | reason = "Exception was raised while handling custom resource." 55 | reason += " Message {}".format(e.args or e.message) 56 | response.update({ 57 | "Status": FAILED, 58 | "Reason": reason, 59 | "Data": {'FailureReason': reason} 60 | }) 61 | 62 | serialized = json.dumps(response) 63 | logger.info("Responding to '%s' request with: %s" % ( 64 | event['RequestType'], serialized)) 65 | 66 | req = requests.put( 67 | event['ResponseURL'], data=serialized, 68 | headers={'Content-Length': str(len(serialized)), 69 | 'Content-Type': ''} 70 | ) 71 | 72 | try: 73 | req 74 | logger.debug("Request to CFN API succeeded, nothing to do here") 75 | except requests.HTTPError as e: 76 | logger.error("Callback to CFN API failed with status %d" % e.code) 77 | logger.error("Response: %s" % e.reason) 78 | except requests.ConnectionError as e: 79 | logger.error("Failed to reach the server - %s" % e.reason) 80 | 81 | return wrapper_func 82 | 83 | 84 | class Resource(object): 85 | _dispatch = None 86 | 87 | def __init__(self, wrapper=wrap_user_handler): 88 | self._dispatch = {} 89 | self._wrapper = wrapper 90 | 91 | def __call__(self, event, context): 92 | request = event['RequestType'] 93 | logger.debug("Received {} type event. Full parameters: {}".format(request, json.dumps(event))) 94 | return self._dispatch.get(request, self._succeed())(event, context) 95 | 96 | def _succeed(self): 97 | @self._wrapper 98 | def success(event, context): 99 | return { 100 | 'Status': SUCCESS, 101 | 'PhysicalResourceId': event.get('PhysicalResourceId', 'none-physical-resource-id'), 102 | 'Reason': 'Request type {} is unknown'.format(event['RequestType']), 103 | 'Data': {} 104 | } 105 | return success 106 | 107 | def create(self, wraps): 108 | self._dispatch['Create'] = self._wrapper(wraps) 109 | return wraps 110 | 111 | def update(self, wraps): 112 | self._dispatch['Update'] = self._wrapper(wraps) 113 | return wraps 114 | 115 | def delete(self, wraps): 116 | self._dispatch['Delete'] = self._wrapper(wraps) 117 | return wraps 118 | 119 | 120 | class NoResponse(Exception): 121 | pass 122 | -------------------------------------------------------------------------------- /code/helpers/defaults.py: -------------------------------------------------------------------------------- 1 | PORTFOLIO_ID_TAG = 'aws:servicecatalog:portfolioArn' 2 | 3 | APPLICABLE_FILE = 'configuration/resource_types_supported.json' 4 | ACTIONS_FILE = 'configuration/resource_types_actions_allowed.json' 5 | ALLOWED_ACTIONS = "*" 6 | 7 | BASE_EMPTY_POLICY = '''{ 8 | "Version": "2012-10-17", 9 | "Statement": [ 10 | { 11 | "Effect": "Deny", 12 | "Action": "*", 13 | "Resource": "arn:aws:iam:::policy/notexist-2h46v9nd84hd7ndir4yd" 14 | } 15 | ] 16 | } 17 | ''' 18 | BASE_POLICY = BASE_EMPTY_POLICY 19 | 20 | BASE_TRUST_POLICY = '''{ 21 | "Version": "2012-10-17", 22 | "Statement": [ 23 | { 24 | "Effect": "Allow", 25 | "Principal": { 26 | "Service": "lambda.amazonaws.com" 27 | }, 28 | "Action": "sts:AssumeRole" 29 | } 30 | ] 31 | } 32 | ''' 33 | -------------------------------------------------------------------------------- /code/helpers/exceptions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | class ResourceNotSupportedError(Exception): 18 | 19 | def __init__(self, resource_type): 20 | super(ResourceNotSupportedError, self).__init__() 21 | self.message = 'Resource of type {} is not supported'.format(resource_type) 22 | 23 | 24 | class MissingTrustRelationshipError(Exception): 25 | 26 | def __init__(self): 27 | super(MissingTrustRelationshipError, self).__init__() 28 | self.message = 'Trust Relationship are missing' 29 | 30 | 31 | class TagNotExist(Exception): 32 | 33 | def __init__(self, key): 34 | super(TagNotExist, self).__init__() 35 | self.message = 'Tag {} does not exist'.format(key) 36 | 37 | 38 | class EventParsingError(Exception): 39 | 40 | def __init__(self, key): 41 | super(EventParsingError, self).__init__() 42 | self.message = 'Failed to parse key {} from the event'.format(key) 43 | 44 | 45 | class FailedToFindOrCreateResource(Exception): 46 | 47 | Message = 'Failed to find or create resource with {}: {}' 48 | 49 | def __init__(self, field, value): 50 | super(FailedToFindOrCreateResource, self).__init__() 51 | self.message = self.Message.format(field, value) 52 | 53 | 54 | class InvalidConfiguration(Exception): 55 | 56 | Message = 'Resource Configuration missed resource types: {}' 57 | 58 | def __init__(self, resource_types): 59 | super(InvalidConfiguration, self).__init__() 60 | self.message = self.Message.format(resource_types) 61 | 62 | -------------------------------------------------------------------------------- /code/helpers/logger.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import logging 18 | import os 19 | 20 | LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') 21 | logger = logging.getLogger() 22 | logger.setLevel(getattr(logging, LOG_LEVEL)) 23 | -------------------------------------------------------------------------------- /code/helpers/partition.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from helpers.exceptions import MissingTrustRelationshipError 18 | from helpers.logger import logger 19 | from helpers.controllers.policies import Policies 20 | from helpers.controllers.policy import Policy 21 | from helpers.controllers.roles import Roles 22 | from helpers.controllers.role import Role 23 | from helpers.controllers.table import Table 24 | from helpers.defaults import BASE_POLICY 25 | from helpers.defaults import BASE_TRUST_POLICY 26 | 27 | 28 | class Partition(object): 29 | 30 | def __init__(self, portfolio_id, table_name): 31 | self.table = Table(table_name) 32 | self.policies = Policies() 33 | policy_arn = self.policies.find(portfolio_id, portfolio_id) \ 34 | or self.policies.create(portfolio_id, portfolio_id, BASE_POLICY) 35 | self.policy = Policy(policy_arn) 36 | self.roles = Roles() 37 | role_arn = self.roles.find(portfolio_id) \ 38 | or self.roles.create(portfolio_id, portfolio_id, BASE_TRUST_POLICY) 39 | self.role = Role(role_arn) 40 | self.role.attach_policy(self.policy.arn) 41 | 42 | def include(self, resource_generator): 43 | items = [] 44 | for resource in resource_generator: 45 | logger.debug('include resource {}'.format(resource.arn)) 46 | if resource.access_to: 47 | items.append({ 48 | 'arn': resource.arn, 49 | 'policy': self.policy.arn, 50 | 'sid': resource.statement_id, 51 | 'actions': resource.actions 52 | }) 53 | if resource.assumed_role: 54 | resource.attach_policy(self.policy.arn) 55 | if resource.access_from and not resource.assumed_role: 56 | try: 57 | resource.assume_role(self.role.arn) 58 | except MissingTrustRelationshipError: 59 | self.role.update_trust_document( 60 | resource.trust_relationship_service 61 | ) 62 | if resource.service_name != 'ec2': 63 | resource.assume_role(self.role.arn) 64 | if len(items) > 0: 65 | logger.info('Add resources {}'.format(items)) 66 | self.table.add(items) 67 | 68 | def exclude(self, resource_generator): 69 | items = [] 70 | for resource in resource_generator: 71 | logger.debug('include resource {}'.format(resource.arn)) 72 | if resource.arn: 73 | items.append({ 74 | 'policy': self.policy.arn, 75 | 'arn': resource.arn 76 | }) 77 | if resource.assumed_role != self.role.name: 78 | resource.detach_policy(self.policy.arn) 79 | else: 80 | resource.stop_assume_role(self.role.name) 81 | if len(items) > 0: 82 | logger.info('Remove resources {}'.format(items)) 83 | self.table.remove(items) 84 | -------------------------------------------------------------------------------- /code/helpers/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/code/helpers/resources/__init__.py -------------------------------------------------------------------------------- /code/helpers/resources/base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from boto3.exceptions import ResourceNotExistsError 18 | from helpers.utils.catch_error import catch_error 19 | from helpers.utils.session import Session 20 | from helpers.utils import convertor 21 | 22 | 23 | class Base(object): 24 | 25 | def __init__(self, physical_id, **kwargs): 26 | self.physical_id = physical_id 27 | 28 | self._session = Session() 29 | self._client = self._session.client(self._service_name()) 30 | self._resource = self._resource() 31 | self._iam_client = self._session.client('iam') 32 | self._iam_resource = self._session.resource('iam') 33 | 34 | self.arn = self._arn() 35 | self.service_name = self._service_name() 36 | self.access_to = self._access_to() 37 | self.access_from = self._access_from() 38 | self.assumed_role = self._assumed_role() 39 | self.trust_relationship_service = self._trust_relationship_service() 40 | self.statement_id = self._statement_id() 41 | self.actions = kwargs.get('actions', self._actions()) 42 | 43 | def __str__(self): 44 | return "Resource id: {} Arn: {}".format(self.physical_id, self.arn) 45 | 46 | @catch_error(ResourceNotExistsError) 47 | def _resource(self): 48 | return self._session.resource(self._service_name()) 49 | 50 | def _arn(self): 51 | return None 52 | 53 | def _service_name(self): 54 | return None 55 | 56 | def _access_to(self): 57 | return False 58 | 59 | def _access_from(self): 60 | return False 61 | 62 | def _assumed_role(self): 63 | return None 64 | 65 | def _trust_relationship_service(self): 66 | return None 67 | 68 | def _statement_id(self): 69 | return None 70 | 71 | def _actions(self): 72 | return None 73 | 74 | def attach_policy(self, policy): 75 | if not self.assumed_role: 76 | return 77 | role_name = convertor.arn_to_name(self.assumed_role) 78 | self._iam_client.attach_role_policy( 79 | RoleName=role_name, 80 | PolicyArn=policy 81 | ) 82 | 83 | def assume_role(self, role): 84 | pass 85 | 86 | def detach_policy(self, policy): 87 | if not self.assumed_role: 88 | return 89 | role_name = convertor.arn_to_name(self.assumed_role) 90 | self._iam_client.detach_role_policy( 91 | RoleName=role_name, 92 | PolicyArn=policy 93 | ) 94 | 95 | def stop_assume_role(self, role): 96 | pass 97 | -------------------------------------------------------------------------------- /code/helpers/resources/ec2_instance.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import jmespath 18 | import time 19 | import re 20 | from botocore.exceptions import ClientError 21 | 22 | from helpers.resources.base import Base 23 | from helpers.exceptions import MissingTrustRelationshipError 24 | 25 | 26 | class EC2Instance(Base): 27 | 28 | def _arn(self): 29 | pattern = '[a-z]+-[a-z]+-\d' 30 | jmespath_az = "Reservations[].Instances[?InstanceId=='{}'].\ 31 | Placement.AvailabilityZone | [0][0]".format(self.physical_id) 32 | jmespath_account = "Reservations[].OwnerId | [0]" 33 | data = self._client.describe_instances(InstanceIds=[self.physical_id]) 34 | account = jmespath.search(jmespath_account, data) 35 | az = jmespath.search(jmespath_az, data) 36 | if az and account: 37 | region = re.match(pattern, az).group() 38 | return 'arn:aws:ec2:{region}:{account}:instance/{instance}'.format( 39 | region=region, 40 | account=account, 41 | instance=self.physical_id 42 | ) 43 | return self.physical_id 44 | 45 | def _service_name(self): 46 | return 'ec2' 47 | 48 | def _access_to(self): 49 | return True 50 | 51 | def _access_from(self): 52 | return True 53 | 54 | def _assumed_role(self): 55 | profile_arn = self.get_instance_profile() 56 | if profile_arn: 57 | profile_name = profile_arn.split('/')[-1] 58 | instance_profile = self._iam_resource.InstanceProfile(profile_name) 59 | try: 60 | instance_profile.delete() 61 | return None 62 | except ClientError as e: 63 | if e.response['Error']['Code'] == 'DeleteConflict': 64 | role = instance_profile.roles[0].name 65 | return role 66 | else: 67 | raise e 68 | return None 69 | 70 | def _statement_id(self): 71 | return 'DedicatedForResourceTypeEC2' 72 | 73 | def _trust_relationship_service(self): 74 | return "ec2.amazonaws.com" 75 | 76 | def _actions(self): 77 | return "*" 78 | 79 | def assume_role(self, role): 80 | if self.assumed_role: 81 | return 82 | prefix = role.split('/')[-1] 83 | profile_name = prefix + self.physical_id 84 | try: 85 | profile_arn = self._iam_client.create_instance_profile( 86 | InstanceProfileName=profile_name, 87 | Path='/'+prefix+'/' 88 | )['InstanceProfile']['Arn'] 89 | except ClientError as e: 90 | if e.response['Error']['Code'] == 'EntityAlreadyExists': 91 | profile_arn = self._iam_resource.InstanceProfile(profile_name).arn 92 | try: 93 | self._iam_client.add_role_to_instance_profile( 94 | InstanceProfileName=profile_name, 95 | RoleName=prefix 96 | ) 97 | except ClientError as e: 98 | if e.response['Error']['Code'] == 'InvalidParameterValue': 99 | time.sleep(15) 100 | self._iam_client.add_role_to_instance_profile( 101 | InstanceProfileName=profile_name, 102 | RoleName=prefix 103 | ) 104 | elif e.response['Error']['Code'] == 'LimitExceeded': 105 | pass 106 | else: 107 | raise e 108 | try: 109 | self._client.associate_iam_instance_profile( 110 | IamInstanceProfile={'Arn': profile_arn}, 111 | InstanceId=self.physical_id 112 | ) 113 | except ClientError as e: 114 | if e.response['Error']['Code'] == 'InvalidParameterValue': 115 | time.sleep(15) 116 | self._client.associate_iam_instance_profile( 117 | IamInstanceProfile={'Arn': profile_arn}, 118 | InstanceId=self.physical_id 119 | ) 120 | elif e.response['Error']['Code'] == 'IncorrectState': 121 | pass 122 | else: 123 | raise e 124 | trust_service = jmespath.search( 125 | "Statement[].Principal.Service | contains(@, '{}')".format(self.service_name), 126 | self._iam_resource.Role(prefix).assume_role_policy_document 127 | ) 128 | if not trust_service: 129 | raise MissingTrustRelationshipError() 130 | 131 | def stop_assume_role(self, role): 132 | try: 133 | profile_arn = self.get_instance_profile() 134 | instance_profile = self._iam_resource.InstanceProfile(profile_arn) 135 | instance_profile.remove_role(RoleName=role.split('/')[-1]) 136 | for association in self.get_association_id(): 137 | self._client.disassociate_iam_instance_profile( 138 | AssociationId=association 139 | ) 140 | instance_profile.delete() 141 | except: 142 | pass 143 | 144 | def get_instance_profile(self): 145 | data = self._client.describe_iam_instance_profile_associations( 146 | Filters=[{'Name': 'instance-id', 'Values': [self.physical_id]}, 147 | {'Name': 'state', 'Values': ['associated']}] 148 | ) 149 | profile_arn = jmespath.search( 150 | "IamInstanceProfileAssociations[?InstanceId=='{}']\ 151 | .IamInstanceProfile.Arn | [0]".format(self.physical_id), 152 | data 153 | ) 154 | return profile_arn 155 | 156 | def get_association_id(self): 157 | data = self._client.describe_iam_instance_profile_associations( 158 | Filters=[{'Name': 'instance-id', 'Values': [self.physical_id]}, 159 | {'Name': 'state', 'Values': ['associated']}] 160 | ) 161 | associations = jmespath.search( 162 | "IamInstanceProfileAssociations[?InstanceId=='{}']\ 163 | .AssociationId".format(self.physical_id), 164 | data 165 | ) 166 | return associations 167 | -------------------------------------------------------------------------------- /code/helpers/resources/factory.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from helpers.logger import logger 18 | from helpers.exceptions import ResourceNotSupportedError 19 | RESOURCES_MOD_PATH = 'helpers.resources.{}' 20 | 21 | 22 | class Factory(object): 23 | 24 | def __new__( 25 | cls, 26 | rtype, 27 | physical_id, 28 | resources_mod_path=RESOURCES_MOD_PATH, 29 | **kwargs 30 | ): 31 | rcls = rtype.replace('::', '')[3:] 32 | if 'Custom' in rtype: 33 | rcls = 'Custom' 34 | rtype = 'AWS::Custom' 35 | try: 36 | rmod = __import__( 37 | resources_mod_path.format( 38 | rtype.replace('::', '_')[4:].lower() 39 | ), 40 | globals(), 41 | locals(), 42 | ['{}'.format(rcls)], 43 | 0 44 | ) 45 | except ImportError as e: 46 | if e.__class__ == 'ModuleNotFoundError': 47 | raise ResourceNotSupportedError(rtype) 48 | raise e 49 | instance = getattr(rmod, rcls)(physical_id, **kwargs) 50 | return instance 51 | -------------------------------------------------------------------------------- /code/helpers/resources/lambda_function.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from helpers.resources.base import Base 18 | from botocore.exceptions import ClientError 19 | from helpers.exceptions import MissingTrustRelationshipError 20 | 21 | 22 | class LambdaFunction(Base): 23 | 24 | def _arn(self): 25 | return self._client.get_function( 26 | FunctionName=self.physical_id 27 | )['Configuration']['FunctionArn'] 28 | 29 | def _service_name(self): 30 | return 'lambda' 31 | 32 | def _access_to(self): 33 | return True 34 | 35 | def _access_from(self): 36 | return True 37 | 38 | def _assumed_role(self): 39 | if not self.arn: 40 | return None 41 | role = self._client.get_function_configuration( 42 | FunctionName=self.arn 43 | ).get('Role', None) 44 | if role == '': 45 | return None 46 | return role.split('role/')[-1] 47 | 48 | def _trust_relationship_service(self): 49 | return "lambda.amazonaws.com" 50 | 51 | def _statement_id(self): 52 | return "DedicatedForResourceTypeLambda" 53 | 54 | def _actions(self): 55 | return "*" 56 | 57 | def assume_role(self, role): 58 | if not self.arn: 59 | return None 60 | try: 61 | self._client.update_function_configuration( 62 | FunctionName=self.arn, 63 | Role=role 64 | ) 65 | except ClientError as e: 66 | if e.response['Error']['Code'] == 'InvalidParameterValueException': 67 | if e.response['Error']['Message'] == '\ 68 | The role defined for the function cannot be assumed\ 69 | by Lambda.': 70 | raise MissingTrustRelationshipError() 71 | 72 | def stop_assume_role(self, role): 73 | pass 74 | -------------------------------------------------------------------------------- /code/helpers/resources/s3_bucket.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from helpers.resources.base import Base 18 | 19 | 20 | class S3Bucket(Base): 21 | 22 | def _arn(self): 23 | bucket_name = self.physical_id.split(':')[-1] 24 | return 'arn:aws:s3:::{}'.format(bucket_name) 25 | 26 | def _service_name(self): 27 | return 's3' 28 | 29 | def _access_to(self): 30 | return True 31 | 32 | def _access_from(self): 33 | return False 34 | 35 | def _assumed_role(self): 36 | return None 37 | 38 | def _trust_relationship_service(self): 39 | return "s3.amazonaws.com" 40 | 41 | def _statement_id(self): 42 | return "DedicatedForResourceTypeS3" 43 | 44 | def _actions(self): 45 | return "*" 46 | -------------------------------------------------------------------------------- /code/helpers/resources_generator.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import json 18 | import re 19 | from pathlib import Path 20 | from helpers.resources.factory import Factory 21 | from helpers.defaults import APPLICABLE_FILE, ACTIONS_FILE 22 | from helpers.defaults import ALLOWED_ACTIONS 23 | from helpers.exceptions import InvalidConfiguration 24 | 25 | 26 | class ResourcesGenerator(object): 27 | 28 | def __init__( 29 | self, 30 | stack_resources, 31 | resources_fp=APPLICABLE_FILE, 32 | actions_fp=ACTIONS_FILE 33 | ): 34 | self.file_applicable = Path(resources_fp).resolve().as_posix() 35 | self._applicable = None 36 | self.file_allowed_actions = Path(actions_fp).resolve().as_posix() 37 | self._actions = None 38 | self.stack_resources = stack_resources 39 | 40 | @property 41 | def applicability(self): 42 | if not self._applicable: 43 | with open(self.file_applicable, 'r') as stream: 44 | self._applicable = json.loads(stream.read()) 45 | return self._applicable 46 | 47 | @property 48 | def actions(self): 49 | if not self._actions: 50 | with open(self.file_allowed_actions, 'r') as stream: 51 | self._actions = json.loads(stream.read()) 52 | return self._actions 53 | 54 | def applicable(self, resource_type): 55 | if resource_type in self.applicability['Applicable']: 56 | return True 57 | for non_applicable_rtype in self.applicability['NotApplicable']: 58 | if re.search( 59 | non_applicable_rtype, 60 | resource_type, 61 | flags=re.IGNORECASE 62 | ): 63 | return False 64 | 65 | def validate_configuration(self): 66 | missing_types = [] 67 | for stack_resource in self.stack_resources: 68 | if self.applicable(stack_resource.resource_type) in (True, False): 69 | continue 70 | else: 71 | missing_types.append(stack_resource.resource_type) 72 | if missing_types: 73 | print('missing types : {}'.format(missing_types)) 74 | raise InvalidConfiguration(missing_types) 75 | 76 | def allowed_actions(self, resource_type): 77 | return self.actions.get(resource_type, ALLOWED_ACTIONS) 78 | 79 | def generator(self): 80 | for stack_resource in self.stack_resources: 81 | if not self.applicable(stack_resource.resource_type): 82 | continue 83 | resource = Factory( 84 | stack_resource.resource_type, 85 | stack_resource.physical_resource_id, 86 | actions=self.allowed_actions(stack_resource.resource_type) 87 | ) 88 | if not resource: 89 | continue 90 | yield resource 91 | -------------------------------------------------------------------------------- /code/helpers/stack.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | 18 | import jmespath 19 | from helpers.utils.session import Session 20 | from helpers.exceptions import TagNotExist 21 | 22 | NOT_APPLICABLE_RESOURCE_STATUS = ('DELETE_COMPLETE', 'CREATE_FAILED') 23 | APPLICABLE_RESOURCE_STATUS = ('CREATE_COMPLETE', 'DELETE_FAILED', 'UPDATE_COMPLETE') 24 | 25 | 26 | class Stack(object): 27 | 28 | def __init__(self, stack_id): 29 | self.session = Session() 30 | self.arn = stack_id 31 | cfn_resource = self.session.resource('cloudformation') 32 | self.aro = cfn_resource.Stack(self.arn) 33 | 34 | def tag(self, key, exception=False): 35 | value = jmespath.search( 36 | "[?Key=='{}'].Value|[0]".format(key), 37 | self.aro.tags 38 | ) 39 | if not value: 40 | if exception: 41 | raise TagNotExist(key) 42 | return value 43 | 44 | def stack_resources(self): 45 | resources = [] 46 | for resource in self.aro.resource_summaries.all(): 47 | stack_resource = self.aro.Resource(resource.logical_id) 48 | if stack_resource.resource_status not in NOT_APPLICABLE_RESOURCE_STATUS: 49 | resources.append(stack_resource) 50 | return resources 51 | 52 | # TODO conclude the new created and the deleted and the replaced resources 53 | def stack_resources_change(self): 54 | resources = [] 55 | for resource in self.aro.resource_summaries.all(): 56 | resource_obj = self.aro.Resource(resource.logical_id) 57 | resources.append(resource_obj) 58 | return resources 59 | -------------------------------------------------------------------------------- /code/helpers/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/code/helpers/utils/__init__.py -------------------------------------------------------------------------------- /code/helpers/utils/catch_error.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | from functools import wraps 18 | from botocore.exceptions import ClientError 19 | from helpers.logger import logger 20 | 21 | 22 | def catch_error(error, handler=None): 23 | def decorator(func): 24 | @wraps(func) 25 | def wrapper(*args, **kwargs): 26 | try: 27 | return func(*args, **kwargs) 28 | except error as err: 29 | if handler: 30 | return handler() 31 | else: 32 | logger.debug( 33 | 'Error {} was ignored while execute function {}'. 34 | format(err, func.__name__) 35 | ) 36 | return None 37 | return wrapper 38 | return decorator 39 | 40 | 41 | def client_error_ignore(code): 42 | def decorator(func): 43 | @wraps(func) 44 | def wrapper(*args, **kwargs): 45 | try: 46 | return func(*args, **kwargs) 47 | except ClientError as err: 48 | if err.response['Error']['Code'] == code: 49 | logger.debug( 50 | 'ClientError {} was ignored while execute function {}'. 51 | format(code, func.__name__) 52 | ) 53 | return None 54 | else: 55 | raise err 56 | return wrapper 57 | return decorator 58 | 59 | 60 | def client_error_handle(code, handler): 61 | def decorator(func): 62 | @wraps(func) 63 | def wrapper(*args, **kwargs): 64 | try: 65 | return func(*args, **kwargs) 66 | except ClientError as err: 67 | if err.response['Error']['Code'] == code: 68 | return handler(*args, **kwargs) 69 | else: 70 | raise err 71 | return wrapper 72 | return decorator 73 | 74 | 75 | def client_error_retry(code, resolver): 76 | def decorator(func): 77 | @wraps(func) 78 | def wrapper(*args, **kwargs): 79 | try: 80 | return func(*args, **kwargs) 81 | except ClientError as err: 82 | if err.response['Error']['Code'] == code: 83 | resolver(args[0]) 84 | return func(*args, **kwargs) 85 | else: 86 | raise err 87 | return wrapper 88 | return decorator 89 | 90 | 91 | def client_error_wait_and_retry(code): 92 | def decorator(func): 93 | @wraps(func) 94 | def wrapper(*args, **kwargs): 95 | try: 96 | return func(*args, **kwargs) 97 | except ClientError as err: 98 | if err.response['Error']['Code'] == code: 99 | return func(*args, **kwargs) 100 | else: 101 | raise err 102 | return wrapper 103 | return decorator 104 | -------------------------------------------------------------------------------- /code/helpers/utils/convertor.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | def arn_to_name(arn): 18 | return arn.split(':')[-1].split('/')[-1] 19 | -------------------------------------------------------------------------------- /code/helpers/utils/rand.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import random 18 | import string 19 | 20 | 21 | def rand(length): 22 | return ''.join( 23 | random.choices( 24 | string.ascii_uppercase + string.ascii_lowercase + string.digits, 25 | k=length 26 | ) 27 | ) 28 | -------------------------------------------------------------------------------- /code/helpers/utils/session.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import boto3 18 | 19 | 20 | class Session(object): 21 | session = None 22 | 23 | def __new__(cls): 24 | if not cls.session: 25 | cls.session = boto3.session.Session() 26 | return cls.session 27 | -------------------------------------------------------------------------------- /code/partition_phase_a.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import os 18 | from helpers import custom_resource_handler 19 | from helpers.stack import Stack 20 | from helpers.partition import Partition 21 | from helpers.resources_generator import ResourcesGenerator 22 | from helpers.defaults import PORTFOLIO_ID_TAG 23 | 24 | TABLE = os.environ['IAM_POLICY_TABLE'] 25 | 26 | handler = custom_resource_handler.Resource() 27 | 28 | 29 | @handler.create 30 | def add(event, context): 31 | stack = Stack(event['StackId']) 32 | portfolio_arn = stack.tag(PORTFOLIO_ID_TAG, exception=True) 33 | portfolio_id = portfolio_arn.split('/')[-1] 34 | 35 | partition = Partition(portfolio_id, TABLE) 36 | resources_generator = ResourcesGenerator(stack.stack_resources()).generator() 37 | partition.include(resources_generator) 38 | 39 | return {} 40 | 41 | 42 | # TODO update policy according to resources status 43 | @handler.update 44 | def modify(event, context): 45 | stack = Stack(event['StackId']) 46 | portfolio_arn = stack.tag(PORTFOLIO_ID_TAG, exception=True) 47 | portfolio_id = portfolio_arn.split('/')[-1] 48 | 49 | partition = Partition(portfolio_id, TABLE) 50 | resources_generator = ResourcesGenerator(stack.stack_resources()).generator() 51 | partition.include(resources_generator) 52 | partition.exclude(resources_generator) 53 | 54 | return {} 55 | 56 | 57 | @handler.delete 58 | def remove(event, context): 59 | stack = Stack(event['StackId']) 60 | portfolio_arn = stack.tag(PORTFOLIO_ID_TAG, exception=True) 61 | portfolio_id = portfolio_arn.split('/')[-1] 62 | 63 | partition = Partition(portfolio_id, TABLE) 64 | resources_generator = ResourcesGenerator(stack.stack_resources()).generator() 65 | partition.exclude(resources_generator) 66 | 67 | return {} 68 | -------------------------------------------------------------------------------- /code/partition_phase_b.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | 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 IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | ''' 16 | 17 | import jmespath 18 | import json 19 | from boto3.dynamodb.conditions import Attr 20 | 21 | from helpers.utils.session import Session 22 | from helpers.logger import logger 23 | from helpers.defaults import BASE_POLICY 24 | from helpers.exceptions import EventParsingError 25 | from helpers.controllers.policy import Policy 26 | from helpers.controllers.role import Role 27 | from helpers.utils import convertor 28 | 29 | session = Session() 30 | dynamodb_resource = session.resource('dynamodb') 31 | 32 | 33 | def handler(event, context): 34 | logger.debug(event) 35 | 36 | table_arn = jmespath.search( 37 | "Records[?eventSource=='aws:dynamodb'].eventSourceARN | [0]", 38 | event 39 | ).split('/stream/')[0] 40 | if not table_arn: 41 | raise(EventParsingError('eventSourceARN')) 42 | table = dynamodb_resource.Table(convertor.arn_to_name(table_arn)) 43 | 44 | altered_policies = jmespath.search( 45 | "Records[?eventSource=='aws:dynamodb'].dynamodb.Keys.policy.S", 46 | event 47 | ) 48 | # TODO handle scan api call limit 49 | for policy in altered_policies: 50 | items = table.scan(FilterExpression=Attr('policy').eq(policy))['Items'] 51 | policy_resource = Policy(policy) 52 | if not items: 53 | policy_resource.delete() 54 | Role(policy.replace('policy', 'role')).delete() 55 | else: 56 | policy_document = table_items_to_document(items) 57 | policy_resource.update_document(policy_document) 58 | 59 | 60 | def table_items_to_document(items): 61 | statements = [] 62 | sids = set(jmespath.search("[].sid", items)) 63 | for sid in sids: 64 | actions = list( 65 | set( 66 | jmespath.search( 67 | "[?sid=='{}'].actions | [][]".format(sid), 68 | items 69 | ) 70 | ) 71 | ) 72 | resources = jmespath.search("[?sid=='{}'].arn".format(sid), items) 73 | statements.append( 74 | { 75 | 'Sid': sid, 76 | 'Resource': resources, 77 | 'Action': actions, 78 | 'Effect': 'Allow' 79 | } 80 | ) 81 | if statements: 82 | return { 83 | "Version": "2012-10-17", 84 | "Statement": statements 85 | } 86 | else: 87 | return json.loads(BASE_POLICY) 88 | -------------------------------------------------------------------------------- /demo/demo.md: -------------------------------------------------------------------------------- 1 | 1. Run demo.sh: 2 | ``` 3 | cd aws-service-catalog-portfolio-security-partition/ 4 | ./demo/demo.sh build my-aws-profile us-east-1 5 | ``` 6 | 1. console login as scuserUranus 7 | create stack with 8 | template: `https://s3.amazonaws.com/service-catalog-portfolio-security-partition-us-east-1/demo/launch_productsA.yml` 9 | name: SC-Uarnus-launch-products 10 | 1. console login as scuserNeptun 11 | create stack with 12 | template: `https://s3.amazonaws.com/service-catalog-portfolio-security-partition-us-east-1/demo/launch_productsB.yml` 13 | name: SC-Neptun-launch-products 14 | 1. Cleanup: to delete all resources: 15 | ``` 16 | cd aws-service-catalog-portfolio-security-partition/ 17 | ./demo/demo.sh destroy my-aws-profile us-east-1 18 | ``` 19 | -------------------------------------------------------------------------------- /demo/demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | profile=$2 4 | region=$3 5 | keypair=sc-demo-key-pair 6 | bucketname=service-catalog-portfolio-security-partition-$region 7 | 8 | if [ $1 = 'build' ] 9 | then 10 | aws --profile $profile --region $region s3 mb s3://$bucketname --region $region 11 | aws --profile $profile --region $region s3 cp ./demo/ s3://$bucketname/demo/ --recursive 12 | cd code/; zip -r code.zip ./*; cd .. 13 | aws --profile $profile --region $region s3 cp ./code/code.zip s3://$bucketname/solution/code.zip 14 | aws --profile $profile --region $region s3 cp ./deployment/template.yml s3://$bucketname/solution/template.yml 15 | aws --profile $profile --region $region ec2 create-key-pair --key-name $keypair 16 | aws --profile $profile --region $region cloudformation \ 17 | create-stack \ 18 | --stack-name security-partition-solution \ 19 | --template-url https://s3.amazonaws.com/$bucketname/solution/template.yml \ 20 | --parameters ParameterKey=LambdaCodeS3Bucket,ParameterValue=$bucketname ParameterKey=LambdaCodeS3Key,ParameterValue=solution/code.zip \ 21 | --capabilities CAPABILITY_NAMED_IAM 22 | aws --profile $profile --region $region cloudformation \ 23 | create-stack \ 24 | --stack-name portfolios \ 25 | --template-url https://s3.amazonaws.com/$bucketname/demo/portfolios.yml \ 26 | --parameters ParameterKey=TemplatesS3Url,ParameterValue=https://s3.amazonaws.com/$bucketname/demo/products/ \ 27 | --capabilities CAPABILITY_NAMED_IAM 28 | fi 29 | 30 | if [ $1 = 'destroy' ] 31 | then 32 | aws --profile $profile --region $region cloudformation \ 33 | delete-stack \ 34 | --stack-name SC-Neptun-launch-products 35 | aws --profile $profile --region $region cloudformation \ 36 | delete-stack \ 37 | --stack-name SC-Uranus-launch-products 38 | 39 | pplist=`aws --profile $profile --region $region servicecatalog scan-provisioned-products --access-level-filter Key=Account,Value=self --query "ProvisionedProducts[].Id" --output text` 40 | for pp in $pplist 41 | do 42 | aws --profile $profile --region $region servicecatalog terminate-provisioned-product --provisioned-product-id $pp --ignore-errors 43 | done 44 | stacklist=`aws --profile $profile --region $region cloudformation list-stacks --stack-status-filter CREATE_FAILED CREATE_COMPLETE ROLLBACK_IN_PROGRESS --query "StackSummaries[].StackName | [?contains(@, 'SC-')]" --output text` 45 | for st in $stacklist 46 | do 47 | aws --profile $profile --region $region cloudformation delete-stack --stack-name $st 48 | done 49 | aws --profile $profile --region $region cloudformation \ 50 | delete-stack \ 51 | --stack-name portfolios 52 | aws --profile $profile --region $region cloudformation \ 53 | delete-stack \ 54 | --stack-name security-partition-solution 55 | aws --profile $profile --region $region s3 rb s3://$bucketname --force 56 | aws --profile $profile --region $region ec2 delete-key-pair --key-name $keypair 57 | 58 | fi 59 | -------------------------------------------------------------------------------- /demo/launch_productsA.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: >- 4 | Launch the following products - 5 | Simple EC Instance 6 | Simple EC Instance With Profile 7 | Simple S3 Bucket 8 | Simple Lambda Function 9 | 10 | Resources: 11 | lp1: 12 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 13 | Properties: 14 | ProvisioningArtifactName: 1.0.1 15 | ProductName: "Simple EC Instance" 16 | ProvisionedProductName: testProductA001 17 | ProvisioningParameters: 18 | - 19 | Key: KeyName 20 | Value: sc-demo-key-pair 21 | lp2: 22 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 23 | Properties: 24 | ProvisioningArtifactName: 1.0.1 25 | ProductName: "Simple EC Instance" 26 | ProvisionedProductName: testProductA002 27 | ProvisioningParameters: 28 | - 29 | Key: KeyName 30 | Value: sc-demo-key-pair 31 | lp3: 32 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 33 | Properties: 34 | ProvisioningArtifactName: 1.0.1 35 | ProductName: "Simple EC Instance With Profile" 36 | ProvisionedProductName: testProductA003 37 | ProvisioningParameters: 38 | - 39 | Key: KeyName 40 | Value: sc-demo-key-pair 41 | lp4: 42 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 43 | Properties: 44 | ProvisioningArtifactName: 1.0.1 45 | ProductName: "Simple EC Instance With Profile" 46 | ProvisionedProductName: testProductA004 47 | ProvisioningParameters: 48 | - 49 | Key: KeyName 50 | Value: sc-demo-key-pair 51 | lp5: 52 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 53 | Properties: 54 | ProvisioningArtifactName: 1.0.1 55 | ProductName: "Simple S3 Bucket" 56 | ProvisionedProductName: testProductA005 57 | lp6: 58 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 59 | Properties: 60 | ProvisioningArtifactName: 1.0.1 61 | ProductName: "Simple S3 Bucket" 62 | ProvisionedProductName: testProductA006 63 | lp7: 64 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 65 | Properties: 66 | ProvisioningArtifactName: 1.0.1 67 | ProductName: "Simple Lambda Function" 68 | ProvisionedProductName: testProductA007 69 | lp8: 70 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 71 | Properties: 72 | ProvisioningArtifactName: 1.0.1 73 | ProductName: "Simple Lambda Function" 74 | ProvisionedProductName: testProductA008 75 | -------------------------------------------------------------------------------- /demo/launch_productsB.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: >- 4 | Launch the following products - 5 | Simple EC Instance 6 | Simple EC Instance With Profile 7 | Simple S3 Bucket 8 | Simple Lambda Function 9 | 10 | Resources: 11 | lp1: 12 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 13 | Properties: 14 | ProvisioningArtifactName: 1.0.1 15 | ProductName: "Simple EC Instance" 16 | ProvisionedProductName: testProductB001 17 | ProvisioningParameters: 18 | - 19 | Key: KeyName 20 | Value: sc-demo-key-pair 21 | lp2: 22 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 23 | Properties: 24 | ProvisioningArtifactName: 1.0.1 25 | ProductName: "Simple EC Instance" 26 | ProvisionedProductName: testProductB002 27 | ProvisioningParameters: 28 | - 29 | Key: KeyName 30 | Value: sc-demo-key-pair 31 | lp3: 32 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 33 | Properties: 34 | ProvisioningArtifactName: 1.0.1 35 | ProductName: "Simple EC Instance With Profile" 36 | ProvisionedProductName: testProductB003 37 | ProvisioningParameters: 38 | - 39 | Key: KeyName 40 | Value: sc-demo-key-pair 41 | lp4: 42 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 43 | Properties: 44 | ProvisioningArtifactName: 1.0.1 45 | ProductName: "Simple EC Instance With Profile" 46 | ProvisionedProductName: testProductB004 47 | ProvisioningParameters: 48 | - 49 | Key: KeyName 50 | Value: sc-demo-key-pair 51 | lp5: 52 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 53 | Properties: 54 | ProvisioningArtifactName: 1.0.1 55 | ProductName: "Simple S3 Bucket" 56 | ProvisionedProductName: testProductB005 57 | lp6: 58 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 59 | Properties: 60 | ProvisioningArtifactName: 1.0.1 61 | ProductName: "Simple S3 Bucket" 62 | ProvisionedProductName: testProductB006 63 | lp7: 64 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 65 | Properties: 66 | ProvisioningArtifactName: 1.0.1 67 | ProductName: "Simple Lambda Function" 68 | ProvisionedProductName: testProductB007 69 | lp8: 70 | Type: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" 71 | Properties: 72 | ProvisioningArtifactName: 1.0.1 73 | ProductName: "Simple Lambda Function" 74 | ProvisionedProductName: testProductB008 75 | -------------------------------------------------------------------------------- /demo/portfolios.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: Describes two sample portfolios which associated with four different products each. 4 | 5 | Parameters: 6 | TemplatesS3Url: 7 | Default: "https://s3.amazonaws.com/sc-security-partition/demo/products/" 8 | Type: String 9 | # https://s3.amazonaws.com/sc-security-partition/demo/launch_productsA.yml 10 | Resources: 11 | SamplePortfolioA: 12 | Type: "AWS::ServiceCatalog::Portfolio" 13 | Properties: 14 | AcceptLanguage: en 15 | Description: Portfolio for Uranus project 16 | DisplayName: Uranus 17 | ProviderName: AWS 18 | Tags: 19 | - Key: Project 20 | Value: Uranus 21 | - Key: Team 22 | Value: SolarSystem 23 | 24 | LaunchRoleAProductSimpleEC2: 25 | DependsOn: AssociatePortfolioAProductEC2 26 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 27 | Properties: 28 | Description: Launch role constraint for ProductSimpleEC2 in SamplePortfolioA 29 | AcceptLanguage: en 30 | PortfolioId: !Ref SamplePortfolioA 31 | ProductId: !Ref ProductSimpleEC2 32 | RoleArn: !GetAtt LaunchRole.Arn 33 | LaunchRoleAProductSimpleEC2WithProfile: 34 | DependsOn: AssociatePortfolioAProductEC2WithProfile 35 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 36 | Properties: 37 | Description: Launch role constraint for ProductSimpleEC2WithProfile in SamplePortfolioA 38 | AcceptLanguage: en 39 | PortfolioId: !Ref SamplePortfolioA 40 | ProductId: !Ref ProductSimpleEC2WithProfile 41 | RoleArn: !GetAtt LaunchRole.Arn 42 | LaunchRoleAProductS3Bucket: 43 | DependsOn: AssociatePortfolioAProductS3 44 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 45 | Properties: 46 | Description: Launch role constraint for ProductS3Bucket in SamplePortfolioA 47 | AcceptLanguage: en 48 | PortfolioId: !Ref SamplePortfolioA 49 | ProductId: !Ref ProductS3Bucket 50 | RoleArn: !GetAtt LaunchRole.Arn 51 | LaunchRoleAProductLambdaFunction: 52 | DependsOn: AssociatePortfolioAProductLambda 53 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 54 | Properties: 55 | Description: Launch role constraint for ProductLambdaFunction in SamplePortfolioA 56 | AcceptLanguage: en 57 | PortfolioId: !Ref SamplePortfolioA 58 | ProductId: !Ref ProductLambdaFunction 59 | RoleArn: !GetAtt LaunchRole.Arn 60 | PortfolioAPrincipal: 61 | Type: "AWS::ServiceCatalog::PortfolioPrincipalAssociation" 62 | Properties: 63 | PrincipalARN: !GetAtt PrincipalA.Arn 64 | AcceptLanguage: en 65 | PortfolioId: !Ref SamplePortfolioA 66 | PrincipalType: IAM 67 | AssociatePortfolioAProductEC2: 68 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 69 | Properties: 70 | PortfolioId: !Ref SamplePortfolioA 71 | ProductId: !Ref ProductSimpleEC2 72 | AssociatePortfolioAProductEC2WithProfile: 73 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 74 | Properties: 75 | PortfolioId: !Ref SamplePortfolioA 76 | ProductId: !Ref ProductSimpleEC2WithProfile 77 | AssociatePortfolioAProductS3: 78 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 79 | Properties: 80 | PortfolioId: !Ref SamplePortfolioA 81 | ProductId: !Ref ProductS3Bucket 82 | AssociatePortfolioAProductLambda: 83 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 84 | Properties: 85 | PortfolioId: !Ref SamplePortfolioA 86 | ProductId: !Ref ProductLambdaFunction 87 | 88 | SamplePortfolioB: 89 | Type: "AWS::ServiceCatalog::Portfolio" 90 | Properties: 91 | AcceptLanguage: en 92 | Description: Portfolio for Neptune project 93 | DisplayName: Neptune 94 | ProviderName: AWS 95 | Tags: 96 | - Key: Project 97 | Value: Neptune 98 | - Key: Team 99 | Value: SolarSystem 100 | LaunchRoleBProductSimpleEC2: 101 | DependsOn: AssociatePortfolioBProductEC2 102 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 103 | Properties: 104 | Description: Launch role constraint for ProductSimpleEC2 in SamplePortfolioB 105 | AcceptLanguage: en 106 | PortfolioId: !Ref SamplePortfolioB 107 | ProductId: !Ref ProductSimpleEC2 108 | RoleArn: !GetAtt LaunchRole.Arn 109 | LaunchRoleBProductSimpleEC2WithProfile: 110 | DependsOn: AssociatePortfolioBProductEC2WithProfile 111 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 112 | Properties: 113 | Description: Launch role constraint for ProductSimpleEC2WithProfile in SamplePortfolioB 114 | AcceptLanguage: en 115 | PortfolioId: !Ref SamplePortfolioB 116 | ProductId: !Ref ProductSimpleEC2WithProfile 117 | RoleArn: !GetAtt LaunchRole.Arn 118 | LaunchRoleBProductS3Bucket: 119 | DependsOn: AssociatePortfolioBProductS3 120 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 121 | Properties: 122 | Description: Launch role constraint for ProductS3Bucket in SamplePortfolioB 123 | AcceptLanguage: en 124 | PortfolioId: !Ref SamplePortfolioB 125 | ProductId: !Ref ProductS3Bucket 126 | RoleArn: !GetAtt LaunchRole.Arn 127 | LaunchRoleBProductLambdaFunction: 128 | DependsOn: AssociatePortfolioBProductLambda 129 | Type: "AWS::ServiceCatalog::LaunchRoleConstraint" 130 | Properties: 131 | Description: Launch role constraint for ProductLambdaFunction in SamplePortfolioB 132 | AcceptLanguage: en 133 | PortfolioId: !Ref SamplePortfolioB 134 | ProductId: !Ref ProductLambdaFunction 135 | RoleArn: !GetAtt LaunchRole.Arn 136 | PortfolioBPrincipal: 137 | Type: "AWS::ServiceCatalog::PortfolioPrincipalAssociation" 138 | Properties: 139 | PrincipalARN: !GetAtt PrincipalB.Arn 140 | AcceptLanguage: en 141 | PortfolioId: !Ref SamplePortfolioB 142 | PrincipalType: IAM 143 | AssociatePortfolioBProductEC2: 144 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 145 | Properties: 146 | PortfolioId: !Ref SamplePortfolioB 147 | ProductId: !Ref ProductSimpleEC2 148 | AssociatePortfolioBProductEC2WithProfile: 149 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 150 | Properties: 151 | PortfolioId: !Ref SamplePortfolioB 152 | ProductId: !Ref ProductSimpleEC2WithProfile 153 | AssociatePortfolioBProductS3: 154 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 155 | Properties: 156 | PortfolioId: !Ref SamplePortfolioB 157 | ProductId: !Ref ProductS3Bucket 158 | AssociatePortfolioBProductLambda: 159 | Type: "AWS::ServiceCatalog::PortfolioProductAssociation" 160 | Properties: 161 | PortfolioId: !Ref SamplePortfolioB 162 | ProductId: !Ref ProductLambdaFunction 163 | 164 | ProductSimpleEC2: 165 | Type: "AWS::ServiceCatalog::CloudFormationProduct" 166 | Properties: 167 | AcceptLanguage: en 168 | Description: Product of simple EC2 Instance 169 | Distributor: AWS 170 | Name: Simple EC Instance 171 | Owner: Engineering Team 172 | SupportEmail: support@engineering.com 173 | SupportUrl: https://www.engineering.com 174 | SupportDescription: For additional support please call 444-3212-0000 175 | ProvisioningArtifactParameters: 176 | - 177 | Description: The first version of this product 178 | Name: 1.0.1 179 | Info: 180 | LoadTemplateFromURL: 181 | 'Fn::Join': 182 | - '' 183 | - - !Ref TemplatesS3Url 184 | - ec2/template.yml 185 | ProductSimpleEC2WithProfile: 186 | Type: "AWS::ServiceCatalog::CloudFormationProduct" 187 | Properties: 188 | AcceptLanguage: en 189 | Description: Product of simple EC2 Instance with attached IAM Instance Profile 190 | Distributor: AWS 191 | Name: Simple EC Instance With Profile 192 | Owner: Engineering Team 193 | SupportEmail: support@engineering.com 194 | SupportUrl: https://www.engineering.com 195 | SupportDescription: For additional support please call 444-3212-0000 196 | ProvisioningArtifactParameters: 197 | - 198 | Description: The first version of this product 199 | Name: 1.0.1 200 | Info: 201 | LoadTemplateFromURL: 202 | 'Fn::Join': 203 | - '' 204 | - - !Ref TemplatesS3Url 205 | - ec2_with_profile/template.yml 206 | ProductS3Bucket: 207 | Type: "AWS::ServiceCatalog::CloudFormationProduct" 208 | Properties: 209 | AcceptLanguage: en 210 | Description: Product contains S3 bucket 211 | Distributor: AWS 212 | Name: Simple S3 Bucket 213 | Owner: Engineering Team 214 | SupportEmail: support@engineering.com 215 | SupportUrl: https://www.engineering.com 216 | SupportDescription: For additional support please call 444-3212-0000 217 | ProvisioningArtifactParameters: 218 | - 219 | Description: The first version of this product 220 | Name: 1.0.1 221 | Info: 222 | LoadTemplateFromURL: 223 | 'Fn::Join': 224 | - '' 225 | - - !Ref TemplatesS3Url 226 | - s3/template.yml 227 | ProductLambdaFunction: 228 | Type: "AWS::ServiceCatalog::CloudFormationProduct" 229 | Properties: 230 | AcceptLanguage: en 231 | Description: Product of lambda function 232 | Distributor: AWS 233 | Name: Simple Lambda Function 234 | Owner: Engineering Team 235 | SupportEmail: support@engineering.com 236 | SupportUrl: https://www.engineering.com 237 | SupportDescription: For additional support please call 444-3212-0000 238 | ProvisioningArtifactParameters: 239 | - 240 | Description: The first version of this product 241 | Name: 1.0.1 242 | Info: 243 | LoadTemplateFromURL: 244 | 'Fn::Join': 245 | - '' 246 | - - !Ref TemplatesS3Url 247 | - lambda/template.yml 248 | 249 | LaunchRole: 250 | Type: "AWS::IAM::Role" 251 | Properties: 252 | AssumeRolePolicyDocument: 253 | Version: 2012-10-17 254 | Statement: 255 | - 256 | Effect: Allow 257 | Principal: 258 | Service: [servicecatalog.amazonaws.com] 259 | Action: 260 | - sts:AssumeRole 261 | ManagedPolicyArns: 262 | - arn:aws:iam::aws:policy/AmazonS3FullAccess 263 | - arn:aws:iam::aws:policy/AmazonEC2FullAccess 264 | - arn:aws:iam::aws:policy/AWSLambdaFullAccess 265 | - arn:aws:iam::aws:policy/IAMFullAccess 266 | - arn:aws:iam::aws:policy/AdministratorAccess 267 | 268 | PrincipalA: 269 | Type: AWS::IAM::User 270 | Properties: 271 | LoginProfile: 272 | Password: myPassword 273 | ManagedPolicyArns: 274 | - arn:aws:iam::aws:policy/AWSServiceCatalogEndUserFullAccess 275 | - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess 276 | Path: "/" 277 | UserName: scuserUranus 278 | 279 | PrincipalB: 280 | Type: AWS::IAM::User 281 | Properties: 282 | LoginProfile: 283 | Password: myPassword 284 | ManagedPolicyArns: 285 | - arn:aws:iam::aws:policy/AWSServiceCatalogEndUserFullAccess 286 | - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess 287 | Path: "/" 288 | UserName: scuserNeptun 289 | 290 | Outputs: 291 | SamplePortfolioA: 292 | Value: !Ref SamplePortfolioA 293 | SamplePortfolioB: 294 | Value: !Ref SamplePortfolioB 295 | Cred: 296 | Value: myP@ssW0rd 297 | -------------------------------------------------------------------------------- /demo/products/ec2/template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: 'AWS CloudFormation Sample Template EC2InstanceWithSecurityGroupSample: 4 | Create an Amazon EC2 instance running the Amazon Linux AMI. The AMI is chosen based 5 | on the region in which the stack is run. This example creates an EC2 security group 6 | for the instance to give you SSH access. **WARNING** This template creates an Amazon 7 | EC2 instance. You will be billed for the AWS resources used if you create a stack 8 | from this template.' 9 | Parameters: 10 | KeyName: 11 | Description: Name of an existing EC2 KeyPair to enable SSH access to the instance 12 | Type: AWS::EC2::KeyPair::KeyName 13 | ConstraintDescription: must be the name of an existing EC2 KeyPair. 14 | InstanceType: 15 | Description: WebServer EC2 instance type 16 | Type: String 17 | Default: t2.small 18 | AllowedValues: 19 | - t1.micro 20 | - t2.nano 21 | - t2.micro 22 | - t2.small 23 | - t2.medium 24 | - t2.large 25 | ConstraintDescription: must be a valid EC2 instance type. 26 | Default: t2.micro 27 | SSHLocation: 28 | Description: The IP address range that can be used to SSH to the EC2 instances 29 | Type: String 30 | MinLength: '9' 31 | MaxLength: '18' 32 | Default: 0.0.0.0/0 33 | AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" 34 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 35 | Mappings: 36 | AWSInstanceType2Arch: 37 | t1.micro: 38 | Arch: PV64 39 | t2.nano: 40 | Arch: HVM64 41 | t2.micro: 42 | Arch: HVM64 43 | t2.small: 44 | Arch: HVM64 45 | t2.medium: 46 | Arch: HVM64 47 | t2.large: 48 | Arch: HVM64 49 | AWSInstanceType2NATArch: 50 | t1.micro: 51 | Arch: NATPV64 52 | t2.nano: 53 | Arch: NATHVM64 54 | t2.micro: 55 | Arch: NATHVM64 56 | t2.small: 57 | Arch: NATHVM64 58 | t2.medium: 59 | Arch: NATHVM64 60 | t2.large: 61 | Arch: NATHVM64 62 | AWSRegionArch2AMI: 63 | us-east-1: 64 | PV64: ami-2a69aa47 65 | HVM64: ami-6869aa05 66 | HVMG2: ami-22b68b59 67 | us-west-2: 68 | PV64: ami-7f77b31f 69 | HVM64: ami-7172b611 70 | HVMG2: ami-be4ea3c6 71 | us-west-1: 72 | PV64: ami-a2490dc2 73 | HVM64: ami-31490d51 74 | HVMG2: ami-cfe5cfaf 75 | eu-west-1: 76 | PV64: ami-4cdd453f 77 | HVM64: ami-f9dd458a 78 | HVMG2: ami-aedb26d7 79 | eu-west-2: 80 | PV64: NOT_SUPPORTED 81 | HVM64: ami-886369ec 82 | HVMG2: NOT_SUPPORTED 83 | eu-central-1: 84 | PV64: ami-6527cf0a 85 | HVM64: ami-ea26ce85 86 | HVMG2: ami-40b8102f 87 | ap-northeast-1: 88 | PV64: ami-3e42b65f 89 | HVM64: ami-374db956 90 | HVMG2: ami-d95aabbf 91 | ap-northeast-2: 92 | PV64: NOT_SUPPORTED 93 | HVM64: ami-2b408b45 94 | HVMG2: NOT_SUPPORTED 95 | ap-southeast-1: 96 | PV64: ami-df9e4cbc 97 | HVM64: ami-a59b49c6 98 | HVMG2: ami-15660276 99 | ap-southeast-2: 100 | PV64: ami-63351d00 101 | HVM64: ami-dc361ebf 102 | HVMG2: ami-0b5a4168 103 | ap-south-1: 104 | PV64: NOT_SUPPORTED 105 | HVM64: ami-ffbdd790 106 | HVMG2: ami-f4cdb79b 107 | us-east-2: 108 | PV64: NOT_SUPPORTED 109 | HVM64: ami-f6035893 110 | HVMG2: NOT_SUPPORTED 111 | ca-central-1: 112 | PV64: NOT_SUPPORTED 113 | HVM64: ami-730ebd17 114 | HVMG2: NOT_SUPPORTED 115 | sa-east-1: 116 | PV64: ami-1ad34676 117 | HVM64: ami-6dd04501 118 | HVMG2: NOT_SUPPORTED 119 | cn-north-1: 120 | PV64: ami-77559f1a 121 | HVM64: ami-8e6aa0e3 122 | HVMG2: NOT_SUPPORTED 123 | Resources: 124 | EC2Instance: 125 | Type: AWS::EC2::Instance 126 | Properties: 127 | InstanceType: 128 | Ref: InstanceType 129 | SecurityGroups: 130 | - Ref: InstanceSecurityGroup 131 | KeyName: 132 | Ref: KeyName 133 | ImageId: 134 | Fn::FindInMap: 135 | - AWSRegionArch2AMI 136 | - Ref: AWS::Region 137 | - Fn::FindInMap: 138 | - AWSInstanceType2Arch 139 | - Ref: InstanceType 140 | - Arch 141 | InstanceSecurityGroup: 142 | Type: AWS::EC2::SecurityGroup 143 | Properties: 144 | GroupDescription: Enable SSH access via port 22 145 | SecurityGroupIngress: 146 | - IpProtocol: tcp 147 | FromPort: '22' 148 | ToPort: '22' 149 | CidrIp: 150 | Ref: SSHLocation 151 | 152 | SetSecurityPartition: 153 | Type: Custom::SetSecurityPartition 154 | DependsOn: EC2Instance 155 | Properties: 156 | ServiceToken: !ImportValue PartitionPhaseAFunctionArn 157 | 158 | Outputs: 159 | InstanceId: 160 | Description: InstanceId of the newly created EC2 instance 161 | Value: 162 | Ref: EC2Instance 163 | AZ: 164 | Description: Availability Zone of the newly created EC2 instance 165 | Value: 166 | Fn::GetAtt: 167 | - EC2Instance 168 | - AvailabilityZone 169 | PublicDNS: 170 | Description: Public DNSName of the newly created EC2 instance 171 | Value: 172 | Fn::GetAtt: 173 | - EC2Instance 174 | - PublicDnsName 175 | PublicIP: 176 | Description: Public IP address of the newly created EC2 instance 177 | Value: 178 | Fn::GetAtt: 179 | - EC2Instance 180 | - PublicIp 181 | -------------------------------------------------------------------------------- /demo/products/ec2_with_profile/template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: 'AWS CloudFormation Sample Template EC2InstanceWithSecurityGroupSample: 4 | Create an Amazon EC2 instance running the Amazon Linux AMI. The AMI is chosen based 5 | on the region in which the stack is run. This example creates an EC2 security group 6 | for the instance to give you SSH access. **WARNING** This template creates an Amazon 7 | EC2 instance. You will be billed for the AWS resources used if you create a stack 8 | from this template.' 9 | Parameters: 10 | KeyName: 11 | Description: Name of an existing EC2 KeyPair to enable SSH access to the instance 12 | Type: AWS::EC2::KeyPair::KeyName 13 | ConstraintDescription: must be the name of an existing EC2 KeyPair. 14 | InstanceType: 15 | Description: WebServer EC2 instance type 16 | Type: String 17 | Default: t2.small 18 | AllowedValues: 19 | - t1.micro 20 | - t2.nano 21 | - t2.micro 22 | - t2.small 23 | - t2.medium 24 | - t2.large 25 | ConstraintDescription: must be a valid EC2 instance type. 26 | Default: t2.micro 27 | SSHLocation: 28 | Description: The IP address range that can be used to SSH to the EC2 instances 29 | Type: String 30 | MinLength: '9' 31 | MaxLength: '18' 32 | Default: 0.0.0.0/0 33 | AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" 34 | ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. 35 | Mappings: 36 | AWSInstanceType2Arch: 37 | t1.micro: 38 | Arch: PV64 39 | t2.nano: 40 | Arch: HVM64 41 | t2.micro: 42 | Arch: HVM64 43 | t2.small: 44 | Arch: HVM64 45 | t2.medium: 46 | Arch: HVM64 47 | t2.large: 48 | Arch: HVM64 49 | AWSInstanceType2NATArch: 50 | t1.micro: 51 | Arch: NATPV64 52 | t2.nano: 53 | Arch: NATHVM64 54 | t2.micro: 55 | Arch: NATHVM64 56 | t2.small: 57 | Arch: NATHVM64 58 | t2.medium: 59 | Arch: NATHVM64 60 | t2.large: 61 | Arch: NATHVM64 62 | AWSRegionArch2AMI: 63 | us-east-1: 64 | PV64: ami-2a69aa47 65 | HVM64: ami-6869aa05 66 | HVMG2: ami-22b68b59 67 | us-west-2: 68 | PV64: ami-7f77b31f 69 | HVM64: ami-7172b611 70 | HVMG2: ami-be4ea3c6 71 | us-west-1: 72 | PV64: ami-a2490dc2 73 | HVM64: ami-31490d51 74 | HVMG2: ami-cfe5cfaf 75 | eu-west-1: 76 | PV64: ami-4cdd453f 77 | HVM64: ami-f9dd458a 78 | HVMG2: ami-aedb26d7 79 | eu-west-2: 80 | PV64: NOT_SUPPORTED 81 | HVM64: ami-886369ec 82 | HVMG2: NOT_SUPPORTED 83 | eu-central-1: 84 | PV64: ami-6527cf0a 85 | HVM64: ami-ea26ce85 86 | HVMG2: ami-40b8102f 87 | ap-northeast-1: 88 | PV64: ami-3e42b65f 89 | HVM64: ami-374db956 90 | HVMG2: ami-d95aabbf 91 | ap-northeast-2: 92 | PV64: NOT_SUPPORTED 93 | HVM64: ami-2b408b45 94 | HVMG2: NOT_SUPPORTED 95 | ap-southeast-1: 96 | PV64: ami-df9e4cbc 97 | HVM64: ami-a59b49c6 98 | HVMG2: ami-15660276 99 | ap-southeast-2: 100 | PV64: ami-63351d00 101 | HVM64: ami-dc361ebf 102 | HVMG2: ami-0b5a4168 103 | ap-south-1: 104 | PV64: NOT_SUPPORTED 105 | HVM64: ami-ffbdd790 106 | HVMG2: ami-f4cdb79b 107 | us-east-2: 108 | PV64: NOT_SUPPORTED 109 | HVM64: ami-f6035893 110 | HVMG2: NOT_SUPPORTED 111 | ca-central-1: 112 | PV64: NOT_SUPPORTED 113 | HVM64: ami-730ebd17 114 | HVMG2: NOT_SUPPORTED 115 | sa-east-1: 116 | PV64: ami-1ad34676 117 | HVM64: ami-6dd04501 118 | HVMG2: NOT_SUPPORTED 119 | cn-north-1: 120 | PV64: ami-77559f1a 121 | HVM64: ami-8e6aa0e3 122 | HVMG2: NOT_SUPPORTED 123 | Resources: 124 | EC2Instance: 125 | Type: AWS::EC2::Instance 126 | Properties: 127 | InstanceType: 128 | Ref: InstanceType 129 | SecurityGroups: 130 | - Ref: InstanceSecurityGroup 131 | KeyName: 132 | Ref: KeyName 133 | ImageId: 134 | Fn::FindInMap: 135 | - AWSRegionArch2AMI 136 | - Ref: AWS::Region 137 | - Fn::FindInMap: 138 | - AWSInstanceType2Arch 139 | - Ref: InstanceType 140 | - Arch 141 | IamInstanceProfile: !Ref InstanceProfile 142 | InstanceSecurityGroup: 143 | Type: AWS::EC2::SecurityGroup 144 | Properties: 145 | GroupDescription: Enable SSH access via port 22 146 | SecurityGroupIngress: 147 | - IpProtocol: tcp 148 | FromPort: '22' 149 | ToPort: '22' 150 | CidrIp: 151 | Ref: SSHLocation 152 | InstanceProfile: 153 | Type: AWS::IAM::InstanceProfile 154 | Properties: 155 | Path: "/" 156 | Roles: 157 | - Ref: InstanceRole 158 | InstanceRole: 159 | Type: AWS::IAM::Role 160 | Properties: 161 | AssumeRolePolicyDocument: 162 | Version: '2012-10-17' 163 | Statement: 164 | - Effect: Allow 165 | Principal: 166 | Service: 167 | - ec2.amazonaws.com 168 | Action: 169 | - sts:AssumeRole 170 | Path: "/" 171 | Policies: 172 | - PolicyName: root 173 | PolicyDocument: 174 | Version: '2012-10-17' 175 | Statement: 176 | - Effect: Allow 177 | Action: 178 | - 'logs:CreateLogGroup' 179 | - 'logs:CreateLogStream' 180 | - 'logs:PutLogEvents' 181 | - 'cloudformation:DescribeStacks' 182 | - 'cloudformation:DescribeStackEvents' 183 | - 'cloudformation:DescribeStackResource' 184 | - 'cloudformation:DescribeStackResources' 185 | - 'cloudformation:GetTemplate' 186 | - 'cloudformation:List*' 187 | - 'servicecatalog:*' 188 | Resource: '*' 189 | 190 | SetSecurityPartition: 191 | Type: Custom::SetSecurityPartition 192 | DependsOn: EC2Instance 193 | Properties: 194 | ServiceToken: !ImportValue PartitionPhaseAFunctionArn 195 | 196 | Outputs: 197 | InstanceId: 198 | Description: InstanceId of the newly created EC2 instance 199 | Value: 200 | Ref: EC2Instance 201 | AZ: 202 | Description: Availability Zone of the newly created EC2 instance 203 | Value: 204 | Fn::GetAtt: 205 | - EC2Instance 206 | - AvailabilityZone 207 | PublicDNS: 208 | Description: Public DNSName of the newly created EC2 instance 209 | Value: 210 | Fn::GetAtt: 211 | - EC2Instance 212 | - PublicDnsName 213 | PublicIP: 214 | Description: Public IP address of the newly created EC2 instance 215 | Value: 216 | Fn::GetAtt: 217 | - EC2Instance 218 | - PublicIp 219 | -------------------------------------------------------------------------------- /demo/products/lambda/template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: Creates a new Lambda Function. 4 | 5 | Resources: 6 | Function: 7 | Type: 'AWS::Lambda::Function' 8 | Properties: 9 | Code: 10 | ZipFile: !Sub | 11 | def handler(event, context): 12 | print "Hello World" 13 | Description: Basic sample lambda function 14 | Handler: index.handler 15 | MemorySize: 1024 16 | Role: !GetAtt 17 | - LambdaExecutionRole 18 | - Arn 19 | Runtime: python2.7 20 | Timeout: 10 21 | LambdaExecutionRole: 22 | Type: 'AWS::IAM::Role' 23 | Properties: 24 | AssumeRolePolicyDocument: 25 | Version: '2012-10-17' 26 | Statement: 27 | - Effect: Allow 28 | Principal: 29 | Service: 30 | - lambda.amazonaws.com 31 | Action: 32 | - 'sts:AssumeRole' 33 | Path: / 34 | Policies: 35 | - PolicyName: root 36 | PolicyDocument: 37 | Version: '2012-10-17' 38 | Statement: 39 | - Effect: Allow 40 | Action: 41 | - 'logs:CreateLogGroup' 42 | - 'logs:CreateLogStream' 43 | - 'logs:PutLogEvents' 44 | Resource: '*' 45 | SetSecurityPartition: 46 | Type: Custom::SetSecurityPartition 47 | DependsOn: Function 48 | Properties: 49 | ServiceToken: !ImportValue PartitionPhaseAFunctionArn 50 | 51 | Outputs: 52 | FunctionName: 53 | Description: The name of the lambda function 54 | Value: !Ref Function 55 | 56 | -------------------------------------------------------------------------------- /demo/products/s3/template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Resources: 4 | S3Bucket: 5 | Type: 'AWS::S3::Bucket' 6 | Properties: 7 | AccessControl: Private 8 | WebsiteConfiguration: 9 | IndexDocument: index.html 10 | ErrorDocument: error.html 11 | DeletionPolicy: Retain 12 | BucketPolicy: 13 | Type: 'AWS::S3::BucketPolicy' 14 | Properties: 15 | PolicyDocument: 16 | Id: MyPolicy 17 | Version: '2012-10-17' 18 | Statement: 19 | - Sid: PublicReadForGetBucketObjects 20 | Effect: Allow 21 | Principal: '*' 22 | Action: 's3:GetObject' 23 | Resource: !Join 24 | - '' 25 | - - 'arn:aws:s3:::' 26 | - !Ref S3Bucket 27 | - /* 28 | Bucket: !Ref S3Bucket 29 | SetSecurityPartition: 30 | DependsOn: BucketPolicy 31 | Type: Custom::SetSecurityPartition 32 | Properties: 33 | ServiceToken: !ImportValue PartitionPhaseAFunctionArn 34 | 35 | Outputs: 36 | WebsiteURL: 37 | Value: !GetAtt 38 | - S3Bucket 39 | - WebsiteURL 40 | Description: URL for website hosted on S3 41 | S3BucketSecureURL: 42 | Value: !Join 43 | - '' 44 | - - 'https://' 45 | - !GetAtt 46 | - S3Bucket 47 | - DomainName 48 | Description: Name of S3 bucket to hold website content 49 | -------------------------------------------------------------------------------- /deployment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/deployment/__init__.py -------------------------------------------------------------------------------- /deployment/template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | 4 | Description: Create all prerequisites for the portfolio security partition solution. 5 | 6 | Parameters: 7 | LambdaCodeS3Bucket: 8 | Description: S3 Bucket Name in which the lambda package stored 9 | Type: String 10 | LambdaCodeS3Key: 11 | Description: S3 Bucket Key of the stored lambda package 12 | Type: String 13 | 14 | Outputs: 15 | PartitionPhaseAFunctionArn: 16 | Description: Lambda function to serve the custom resource of portfolio as security partition 17 | Value: !GetAtt PartitionPhaseAFunction.Arn 18 | Export: 19 | Name: PartitionPhaseAFunctionArn 20 | 21 | Resources: 22 | PartitionPhaseAFunction: 23 | Type: 'AWS::Lambda::Function' 24 | Properties: 25 | Handler: partition_phase_a.handler 26 | Runtime: python3.6 27 | Code: 28 | S3Bucket: !Ref LambdaCodeS3Bucket 29 | S3Key: !Ref LambdaCodeS3Key 30 | Description: 'The first of two lambda functions that applies a security partition 31 | around resources which have originated in the same Service Catalog Portfolio' 32 | MemorySize: 128 33 | Timeout: 30 34 | Role: !GetAtt LambdaPhaseARole.Arn 35 | Environment: 36 | Variables: 37 | LOG_LEVEL: INFO 38 | IAM_POLICY_TABLE: !Ref PolicyTable 39 | LambdaPhaseARole: 40 | Type: 'AWS::IAM::Role' 41 | Properties: 42 | AssumeRolePolicyDocument: 43 | Version: '2012-10-17' 44 | Statement: 45 | - Effect: Allow 46 | Principal: 47 | Service: 48 | - lambda.amazonaws.com 49 | Action: 50 | - 'sts:AssumeRole' 51 | Path: / 52 | Policies: 53 | - PolicyName: root 54 | PolicyDocument: 55 | Version: '2012-10-17' 56 | Statement: 57 | - Effect: Allow 58 | Action: 59 | - 'logs:CreateLogGroup' 60 | - 'logs:CreateLogStream' 61 | - 'logs:PutLogEvents' 62 | - 'cloudformation:DescribeStacks' 63 | - 'cloudformation:DescribeStackEvents' 64 | - 'cloudformation:DescribeStackResource' 65 | - 'cloudformation:DescribeStackResources' 66 | - 'cloudformation:GetTemplate' 67 | - 'cloudformation:List*' 68 | - 'servicecatalog:*' 69 | - 'iam:*' 70 | - 'dynamodb:*' 71 | - 'ec2:Describe*' 72 | - 'ec2:AssociateIamInstanceProfile' 73 | - 'lambda:UpdateFunctionConfiguration' 74 | - 'lambda:GetFunction*' 75 | Resource: '*' 76 | PartitionPhaseBFunction: 77 | Type: 'AWS::Lambda::Function' 78 | Properties: 79 | Handler: partition_phase_b.handler 80 | Runtime: python3.6 81 | Code: 82 | S3Bucket: !Ref LambdaCodeS3Bucket 83 | S3Key: !Ref LambdaCodeS3Key 84 | Description: 'The second of two lambda functions that applies a security partition 85 | around resources which have originated in the same Service Catalog Portfolio' 86 | MemorySize: 128 87 | Timeout: 30 88 | Role: !GetAtt LambdaPhaseBRole.Arn 89 | Environment: 90 | Variables: 91 | LOG_LEVEL: INFO 92 | IAM_POLICY_TABLE: !Ref PolicyTable 93 | LambdaPhaseBRole: 94 | Type: 'AWS::IAM::Role' 95 | Properties: 96 | AssumeRolePolicyDocument: 97 | Version: '2012-10-17' 98 | Statement: 99 | - Effect: Allow 100 | Principal: 101 | Service: 102 | - lambda.amazonaws.com 103 | Action: 104 | - 'sts:AssumeRole' 105 | Path: / 106 | Policies: 107 | - PolicyName: root 108 | PolicyDocument: 109 | Version: '2012-10-17' 110 | Statement: 111 | - Effect: Allow 112 | Action: 113 | - 'logs:CreateLogGroup' 114 | - 'logs:CreateLogStream' 115 | - 'logs:PutLogEvents' 116 | - 'cloudformation:DescribeStacks' 117 | - 'cloudformation:DescribeStackEvents' 118 | - 'cloudformation:DescribeStackResource' 119 | - 'cloudformation:DescribeStackResources' 120 | - 'cloudformation:GetTemplate' 121 | - 'cloudformation:List*' 122 | - 'ec2:CreateNetworkInterface' 123 | - 'ec2:DescribeNetworkInterfaces' 124 | - 'ec2:DeleteNetworkInterface' 125 | - 'dynamodb:*' 126 | - 'iam:*' 127 | Resource: '*' 128 | PolicyTable: 129 | Type: 'AWS::DynamoDB::Table' 130 | Properties: 131 | AttributeDefinitions: 132 | - AttributeName: policy 133 | AttributeType: S 134 | - AttributeName: arn 135 | AttributeType: S 136 | KeySchema: 137 | - AttributeName: policy 138 | KeyType: HASH 139 | - AttributeName: arn 140 | KeyType: RANGE 141 | ProvisionedThroughput: 142 | ReadCapacityUnits: 5 143 | WriteCapacityUnits: 5 144 | StreamSpecification: 145 | StreamViewType: NEW_IMAGE 146 | PolicyTableEventStream: 147 | Type: "AWS::Lambda::EventSourceMapping" 148 | Properties: 149 | BatchSize: 100 150 | EventSourceArn: !GetAtt PolicyTable.StreamArn 151 | FunctionName: !GetAtt PartitionPhaseBFunction.Arn 152 | StartingPosition: TRIM_HORIZON 153 | -------------------------------------------------------------------------------- /docs/custom_resource.md: -------------------------------------------------------------------------------- 1 | ## LAMBDA-BACKED CUSTOM RESOURCE 2 | 3 | When implementing the lambda function, of a Lambda-backed Custom Resource, there are two main considerations to address. First, if the function errored out with exception, it wont response to the cloudformation creation process. As a result the creation process will hang and the Service Catalog consumer will wait for about an hour until the product provisioning will eventually fail - bad UX. Second, after created, the cloudformation stack may be updated or deleted, thus the custom resource lambda function must handle the whole stack (custom resource) life cycle. I.e., “Update” and “Delete” actions. 4 | To mitigate the first risk, the main function should be designed to catch and handle any exception, and always response to cloudformation. Moreover, the exception message should be included in the response, in order for the Service Catalog consumer to get a clue about the failure reason. 5 | -------------------------------------------------------------------------------- /docs/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/docs/images/architecture.png -------------------------------------------------------------------------------- /docs/images/concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/docs/images/concept.png -------------------------------------------------------------------------------- /docs/images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/docs/images/flow.png -------------------------------------------------------------------------------- /docs/images/policy_and_role_as_boundary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/docs/images/policy_and_role_as_boundary.png -------------------------------------------------------------------------------- /docs/images/usecase-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-service-catalog-portfolio-partition/5f72cb419e97ac6684c644e7ddb21c6466cb4401/docs/images/usecase-example.png -------------------------------------------------------------------------------- /docs/support_new_resource_type.md: -------------------------------------------------------------------------------- 1 | # Supporting new Resource Types 2 | When it comes to IAM, each AWS resource type might have a different behavior. Moreover, there are resource types which are actively accessing other resources and thus might assume roles (e.g., ec2 instance, lambda function) while others are more passive (e.g., s3, dynamoDB). To handle the different resources, the portfolio security partition implementation maintain a whitelist of supported resource types. In addition, to be supported, each resource type must implement the resources/base.py interface. 3 | The following document is a step by step guide for adding support for unsupported resource type. 4 | 5 | ## Add support to "AWS::ServiceX::ResY" 6 | To demonstrate the process, we will add a fake aws resource: "AWS::ServiceX::ResY" 7 | 8 | #### Add the new resource type to the whitelist: 9 | Aws-service-catalog-portfolio-partition/code/configuration/resource_types_supported.json 10 | ```json 11 | { 12 | "Applicable": [ 13 | "...", 14 | "...", 15 | "AWS::ServiceX::ResY" 16 | ], 17 | "NotApplicable": [ 18 | "...", 19 | "..." 20 | ] 21 | } 22 | ``` 23 | ##### Applicable: 24 | - For supported resources which must be handled. 25 | - Must be the exact resource type name. 26 | 27 | ##### NotApplicable: 28 | - For resources which should be ignored or not supported. 29 | - Supports regex 30 | 31 | If a resource type appears in the “Applicable” list it will be processed regardless its status in the “NotApplicable” list. 32 | 33 | #### The Interface: 34 | `code/helpers/resources/base.py` 35 | 36 | Create new file: 37 | ```bash 38 | aws-service-catalog-portfolio-partition/code/helpers/resources/servicex_resy.py 39 | ``` 40 | ```python 41 | from helpers.resources.base import Base 42 | 43 | class ServicexResy(Base): 44 | 45 | # returns the ARN of the resource AWS::ServiceX::ResY 46 | def _arn(self): 47 | pass 48 | 49 | # returns the name of the service with which a “boto3.client” and “boto3.resource” will be instantiated 50 | def _service_name(self): 51 | return ‘servicex’ 52 | 53 | # returns True when the resource should be accessed by other resources 54 | def _access_to(self): 55 | return True 56 | 57 | # returns True when the resource should assume role to access other resources 58 | def _access_from(self): 59 | return True 60 | 61 | # returns the arn of a role that the resource currently assumes or None when N/A 62 | def _assumed_role(self): 63 | pass 64 | 65 | # returns the name to be used for the statement id in the policy, to be dedicated for the resources of this type 66 | def _statement_id(self): 67 | return 'DedicatedForResourceTypeServiceXResY' 68 | 69 | # returns the service name as it should appear in a trust relationship policy document 70 | def _trust_relationship_service(self): 71 | return "servicex.amazonaws.com" 72 | 73 | # returns the actions to be allowed on this resource type (will be overridden by resource_types_actions_allowed.json) 74 | def _actions(self): 75 | return "*" 76 | 77 | # assumes the passed role and raise trust relationship missing error if the service isn’t listed in the role trust policy 78 | def assume_role(self, role): 79 | pass 80 | 81 | # detach the assumed role from the resource 82 | def stop_assume_role(self, role): 83 | pass 84 | ``` 85 | 86 | #### Lambda Functions Permissions 87 | The permissions of the lambda functions' execution role must be updated with new allowed actions used in the new resource type object implementation. 88 | This should be done by updating the Aws-service-catalog-portfolio-partition/deployment/template.yml and update the relevant CFN stack. 89 | --------------------------------------------------------------------------------