├── rolemodel ├── _version ├── role.py ├── group.py ├── stack.py └── __init__.py ├── requirements.txt ├── MANIFEST.in ├── sample ├── config.yml ├── users.yml └── roles.cf ├── .gitignore ├── setup.py ├── bin └── rolemodel ├── README.md └── LICENSE /rolemodel/_version: -------------------------------------------------------------------------------- 1 | 0.1.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | botocore==0.82.0 2 | click==3.3 3 | PyYAML>=3.11 4 | mock>=1.0.1 5 | nose==1.3.1 6 | tox==1.7.1 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | include rolemodel/_version 5 | recursive-include sample *.yml *.cf 6 | -------------------------------------------------------------------------------- /sample/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | assumable_roles: roles.cf 3 | stack_name: Example-RoleModel 4 | master_account_id: 123456789012 5 | master_account_profile: dev 6 | assumable_accounts: 7 | - 8 | name: prod 9 | profile: prod 10 | - 11 | name: cdn 12 | profile: cdn -------------------------------------------------------------------------------- /sample/users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | prod: 3 | AdminRole: 4 | - carol@foobar.com 5 | SuperUserRole: 6 | - bob@foobar.com 7 | FinanceRole: 8 | - ted@foobar.com 9 | cdn: 10 | AdminRole: 11 | - carol@foobar.com 12 | SuperUserRole: 13 | - bob@foobar.com 14 | - alice@foobar.com 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # Emacs backup files 57 | *~ -------------------------------------------------------------------------------- /rolemodel/role.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Scopely, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import logging 15 | 16 | LOG = logging.getLogger(__name__) 17 | 18 | 19 | class Role(object): 20 | 21 | def __init__(self, session, data): 22 | self._session = session 23 | self.logical_name = data['LogicalResourceId'] 24 | self.physical_name = data['PhysicalResourceId'] 25 | self.stack_arn = data['StackId'] 26 | self._iam = self._session.create_client('iam') 27 | self._arn = None 28 | 29 | @property 30 | def arn(self): 31 | if self._arn is None: 32 | response = self._iam.get_role( 33 | RoleName=self.physical_name) 34 | LOG.debug(response) 35 | self._arn = response['Role']['Arn'] 36 | LOG.debug('role_arn: %s', self._arn) 37 | return self._arn 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | import os 6 | 7 | requires = [ 8 | 'botocore==0.82.0', 9 | 'click==3.3', 10 | 'PyYAML>=3.11' 11 | ] 12 | 13 | 14 | setup( 15 | name='rolemodel', 16 | version=open(os.path.join('rolemodel', '_version')).read().strip(), 17 | description='A tool to help manage cross-account roles in AWS', 18 | long_description=open('README.md').read(), 19 | author='Mitch Garnaat', 20 | author_email='mitch@garnaat.com', 21 | url='https://github.com/scopely-devops/rolemodel', 22 | packages=find_packages(exclude=['tests*']), 23 | package_data={'rolemodel': ['_version']}, 24 | package_dir={'rolemodel': 'rolemodel'}, 25 | scripts=['bin/rolemodel'], 26 | install_requires=requires, 27 | license=open("LICENSE").read(), 28 | classifiers=( 29 | 'Development Status :: 3 - Alpha', 30 | 'Intended Audience :: Developers', 31 | 'Intended Audience :: System Administrators', 32 | 'Natural Language :: English', 33 | 'License :: OSI Approved :: Apache Software License', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 2.6', 36 | 'Programming Language :: Python :: 2.7', 37 | 'Programming Language :: Python :: 3', 38 | 'Programming Language :: Python :: 3.3', 39 | 'Programming Language :: Python :: 3.4' 40 | ), 41 | ) 42 | -------------------------------------------------------------------------------- /bin/rolemodel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2015 Scopely, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You 5 | # may not use this file except in compliance with the License. A copy of 6 | # the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "license" file accompanying this file. This file is 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 12 | # ANY KIND, either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | import logging 15 | 16 | import yaml 17 | import click 18 | 19 | from rolemodel import RoleModel 20 | 21 | 22 | @click.group() 23 | @click.argument( 24 | 'config', 25 | type=click.File('rb'), 26 | envvar='ROLEMODEL_CONFIG' 27 | ) 28 | @click.option( 29 | '--debug/--no-debug', 30 | default=False, 31 | help='Turn on debugging output' 32 | ) 33 | @click.pass_context 34 | def cli(ctx, config=None, debug=False): 35 | config = yaml.load(config) 36 | ctx.obj['debug'] = debug 37 | ctx.obj['config'] = config 38 | 39 | @cli.command() 40 | @click.pass_context 41 | def update(ctx): 42 | rm = RoleModel(ctx.obj['config'], ctx.obj['debug']) 43 | click.echo('Updating ...') 44 | rm.update() 45 | click.echo('...done') 46 | 47 | 48 | @cli.command() 49 | @click.pass_context 50 | def list(ctx): 51 | rm = RoleModel(ctx.obj['config'], ctx.obj['debug']) 52 | data = rm.list() 53 | for acct in data: 54 | click.echo('Account: %s' % acct) 55 | for role, role_arn in data[acct]: 56 | click.echo('%s (%s)' % (role, role_arn)) 57 | 58 | 59 | @cli.command() 60 | @click.pass_context 61 | def delete(ctx): 62 | rm = RoleModel(ctx.obj['config'], ctx.obj['debug']) 63 | click.echo('Deleting ...') 64 | rm.delete() 65 | click.echo('...done') 66 | 67 | @cli.command() 68 | @click.argument( 69 | 'user-file', 70 | type=click.File('rb') 71 | ) 72 | @click.pass_context 73 | def sync_users(ctx, user_file): 74 | user_file = yaml.load(user_file) 75 | rm = RoleModel(ctx.obj['config'], ctx.obj['debug']) 76 | click.echo('Syncing Users') 77 | rm.sync_users(user_file) 78 | click.echo('...done') 79 | 80 | 81 | if __name__ == '__main__': 82 | cli(obj={}) 83 | -------------------------------------------------------------------------------- /rolemodel/group.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Scopely, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import logging 15 | import json 16 | 17 | LOG = logging.getLogger(__name__) 18 | 19 | 20 | class Group(object): 21 | 22 | def __init__(self, iam, name, path): 23 | self._iam = iam 24 | self.name = name 25 | self.path = path 26 | 27 | def _delete_policies(self): 28 | LOG.info('Deleting policies from group %s', self.name) 29 | response = self._iam.list_group_policies(GroupName=self.name) 30 | LOG.debug(response) 31 | for policy in response['PolicyNames']: 32 | response = self._iam.delete_group_policy( 33 | GroupName=self.name, PolicyName=policy) 34 | LOG.debug(response) 35 | 36 | def _add_policy(self, policy_name, policy_document): 37 | LOG.debug('Adding policy %s to group %s', policy_name, self.name) 38 | response = self._iam.put_group_policy( 39 | GroupName=self.name, PolicyName=policy_name, 40 | PolicyDocument=policy_document) 41 | LOG.debug(response) 42 | 43 | def _check_policy(self, policy_name, policy_document): 44 | response = self._iam.list_group_policies(GroupName=self.name) 45 | LOG.debug(response) 46 | if policy_name in response['PolicyNames']: 47 | LOG.info('Group %s already contains policy %s', 48 | self.name, policy_name) 49 | response = self._iam.get_group_policy( 50 | GroupName=self.name, PolicyName=policy_name) 51 | LOG.debug(response) 52 | new_policy = json.loads(policy_document) 53 | old_policy = response.get('PolicyDocument', '{}') 54 | new_resource = new_policy['Statement'][0]['Resource'] 55 | old_resource = old_policy['Statement'][0]['Resource'] 56 | if new_resource != old_resource: 57 | LOG.debug('Existing policy is referencing wrong role') 58 | self._add_policy(policy_name, policy_document) 59 | else: 60 | self._add_policy(policy_name, policy_document) 61 | 62 | def check(self, group_names=None): 63 | if group_names is None: 64 | response = self._iam.list_groups(PathPrefix=self.path) 65 | LOG.debug(response) 66 | group_names = [g['GroupName'] for g in response['Groups']] 67 | if self.name not in group_names: 68 | LOG.info('creating group %s', self.name) 69 | response = self._iam.create_group( 70 | GroupName=self.name, Path=self.path) 71 | LOG.debug(response) 72 | 73 | def add_policy(self, policy_name, policy_document): 74 | LOG.info('Adding policy %s to group %s', 75 | policy_name, self.name) 76 | self._check_policy(policy_name, policy_document) 77 | 78 | def delete(self): 79 | LOG.info('deleting group %s', self.name) 80 | self._delete_policies() 81 | response = self._iam.delete_group(GroupName=self.name) 82 | LOG.debug(response) 83 | 84 | def sync_users(self, users): 85 | LOG.info('syncing users for group %s to %s', self.name, users) 86 | response = self._iam.get_group(GroupName=self.name) 87 | LOG.debug(response) 88 | current_users = response['Users'] 89 | for user in current_users: 90 | if user['UserName'] not in users: 91 | response = self._iam.remove_user_from_group( 92 | GroupName=self.name, UserName=user['UserName']) 93 | LOG.debug(response) 94 | existing_usernames = [u['UserName'] for u in current_users] 95 | for user_name in users: 96 | if user_name not in existing_usernames: 97 | response = self._iam.add_user_to_group( 98 | GroupName=self.name, UserName=user_name) 99 | LOG.debug(response) 100 | -------------------------------------------------------------------------------- /rolemodel/stack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Scopely, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import logging 15 | import time 16 | 17 | import rolemodel.role 18 | 19 | LOG = logging.getLogger(__name__) 20 | 21 | 22 | class Stack(object): 23 | 24 | completed_states = ('CREATE_COMPLETE', 'UPDATE_COMPLETE') 25 | failed_states = ('ROLLBACK_COMPLETE',) 26 | 27 | def __init__(self, session, name, roles_template, assuming_account_id): 28 | self._session = session 29 | self._name = name 30 | self._template_path = roles_template 31 | self._assuming_account_id = str(assuming_account_id) 32 | self._cfn = self._session.create_client('cloudformation') 33 | 34 | @property 35 | def name(self): 36 | return self._name 37 | 38 | def exists(self): 39 | """ 40 | Does Cloudformation Stack already exist? 41 | """ 42 | try: 43 | response = self._cfn.describe_stacks(StackName=self.name) 44 | LOG.debug('Stack %s exists', self.name) 45 | except Exception: 46 | LOG.debug('Stack %s does not exist', self.name) 47 | response = None 48 | return response 49 | 50 | def wait(self): 51 | done = False 52 | while not done: 53 | time.sleep(1) 54 | response = self._cfn.describe_stacks(StackName=self.name) 55 | LOG.debug(response) 56 | status = response['Stacks'][0]['StackStatus'] 57 | LOG.debug('Stack status is: %s', status) 58 | if status in self.completed_states: 59 | done = True 60 | if status in self.failed_states: 61 | msg = 'Could not create stack %s: %s' % (self.name, status) 62 | raise ValueError(msg) 63 | 64 | def _validate_template(self, template_body): 65 | LOG.debug('validate_template') 66 | response = self._cfn.validate_template(TemplateBody=template_body) 67 | return response['Parameters'] 68 | 69 | def _create(self): 70 | LOG.debug('create_stack: stack_name=%s', self.name) 71 | template_body = open(self._template_path).read() 72 | self._validate_template(template_body) 73 | 74 | response = self._cfn.create_stack( 75 | StackName=self.name, TemplateBody=template_body, 76 | Capabilities=['CAPABILITY_IAM','CAPABILITY_NAMED_IAM'], 77 | Parameters=[{'ParameterKey': 'AssumingAccountID', 78 | 'ParameterValue': self._assuming_account_id, 79 | 'UsePreviousValue': False}]) 80 | LOG.debug(response) 81 | self.wait() 82 | 83 | def _update(self): 84 | LOG.debug('update_stack: stack_name=%s', self.name) 85 | template_body = open(self._template_path).read() 86 | try: 87 | template_parameters = self._validate_template(template_body) 88 | parameters = [{'ParameterKey': 'AssumingAccountID', 89 | 'ParameterValue': self._assuming_account_id, 90 | 'UsePreviousValue': False}] 91 | 92 | # Use previous parameter values for any extra parameters defined in 93 | # the CloudFormation stack. 94 | for p in template_parameters: 95 | parameters.append({'ParameterKey': p['ParameterKey'], 'UsePreviousValue': True}) 96 | 97 | response = self._cfn.update_stack( 98 | StackName=self.name, TemplateBody=template_body, 99 | Capabilities=['CAPABILITY_IAM','CAPABILITY_NAMED_IAM'], 100 | Parameters=parameters) 101 | LOG.debug(response) 102 | except Exception as e: 103 | if 'No updates are to be performed' in str(e): 104 | LOG.info('No Updates Required') 105 | else: 106 | raise 107 | self.wait() 108 | 109 | def update(self): 110 | if self.exists(): 111 | self._update() 112 | else: 113 | self._create() 114 | 115 | def resources(self): 116 | LOG.debug('resources(): stack_name=%s', self.name) 117 | response = self._cfn.describe_stack_resources( 118 | StackName=self.name) 119 | LOG.debug(response) 120 | return response['StackResources'] 121 | 122 | def roles(self): 123 | LOG.debug('roles(): stack_name=%s', self.name) 124 | response = self._cfn.describe_stack_resources( 125 | StackName=self.name) 126 | LOG.debug(response) 127 | roles = [] 128 | for resource in response['StackResources']: 129 | if resource['ResourceType'] == 'AWS::IAM::Role': 130 | role = rolemodel.role.Role(self._session, resource) 131 | roles.append(role) 132 | return roles 133 | 134 | def delete(self): 135 | LOG.debug('delete(): stack_name=%s', self.name) 136 | response = self._cfn.delete_stack(StackName=self.name) 137 | LOG.debug(response) 138 | -------------------------------------------------------------------------------- /rolemodel/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Scopely, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import os 15 | 16 | __version__ = open(os.path.join(os.path.dirname(__file__), '_version')).read() 17 | 18 | import logging 19 | 20 | import botocore.session 21 | 22 | import rolemodel.stack 23 | import rolemodel.group 24 | 25 | LOG = logging.getLogger(__name__) 26 | 27 | DebugFmtString = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 28 | InfoFmtString = '%(message)s' 29 | 30 | AssumeRolePolicy = """{{ 31 | "Version": "2012-10-17", 32 | "Statement": [{{ 33 | "Effect": "Allow", 34 | "Action": ["sts:AssumeRole"], 35 | "Resource": "{arn}" 36 | }}] 37 | }}""" 38 | 39 | 40 | class RoleModel(object): 41 | 42 | GroupName = '{acct}.{role}' 43 | Path = '/RoleModel/' 44 | 45 | def __init__(self, config, debug=False): 46 | if debug: 47 | self.set_logger('rolemodel', logging.DEBUG) 48 | else: 49 | self.set_logger('rolemodel', logging.INFO) 50 | self.config = config 51 | if not 'stack_name' in self.config: 52 | self.config['stack_name'] = "RoleModel" 53 | 54 | def debug(self): 55 | self.set_logger('rolemodel', logging.DEBUG) 56 | 57 | def set_logger(self, logger_name, level=logging.INFO): 58 | """ 59 | Convenience function to quickly configure full debug output 60 | to go to the console. 61 | """ 62 | log = logging.getLogger(logger_name) 63 | log.setLevel(level) 64 | 65 | ch = logging.StreamHandler(None) 66 | ch.setLevel(level) 67 | 68 | # create formatter 69 | if level == logging.INFO: 70 | formatter = logging.Formatter(InfoFmtString) 71 | else: 72 | formatter = logging.Formatter(DebugFmtString) 73 | 74 | # add formatter to ch 75 | ch.setFormatter(formatter) 76 | 77 | # add ch to logger 78 | log.addHandler(ch) 79 | 80 | def update(self): 81 | for acct in self.config['assumable_accounts']: 82 | LOG.info('updating account: %s', acct['name']) 83 | LOG.debug(acct) 84 | session = botocore.session.get_session() 85 | session.profile = acct['profile'] 86 | stack = rolemodel.stack.Stack( 87 | session, self.config['stack_name'], self.config['assumable_roles'], 88 | self.config['master_account_id']) 89 | stack.update() 90 | self.update_groups() 91 | 92 | def delete(self): 93 | LOG.debug('delete called') 94 | for acct in self.config['assumable_accounts']: 95 | LOG.info('deleting %s', acct['name']) 96 | session = botocore.session.get_session() 97 | session.profile = acct['profile'] 98 | stack = rolemodel.stack.Stack( 99 | session, self.config['stack_name'], self.config['assumable_roles'], 100 | self.config['master_account_id']) 101 | stack.delete() 102 | self.delete_groups() 103 | 104 | def list(self): 105 | LOG.debug('list called') 106 | list_data = {} 107 | for acct in self.config['assumable_accounts']: 108 | session = botocore.session.get_session() 109 | session.profile = acct['profile'] 110 | stack = rolemodel.stack.Stack( 111 | session, self.config['stack_name'], self.config['assumable_roles'], 112 | self.config['master_account_id']) 113 | if stack.exists(): 114 | list_data[acct['name']] = [ 115 | (r.physical_name, r.arn) for r in stack.roles()] 116 | return list_data 117 | 118 | def _check_for_groups(self, iam, acct, stack): 119 | groups = iam.list_groups() 120 | group_names = [g['GroupName'] for g in groups['Groups']] 121 | for role in stack.roles(): 122 | group_name = self.GroupName.format( 123 | acct=acct['name'], role=role.logical_name) 124 | group = rolemodel.group.Group( 125 | iam, group_name, self.Path) 126 | group.check(group_names) 127 | policy_name = '%s-Policy' % group_name 128 | policy_document = AssumeRolePolicy.format(arn=role.arn) 129 | group.add_policy(policy_name, policy_document) 130 | LOG.info('creating policy %s', policy_name) 131 | 132 | def update_groups(self): 133 | LOG.debug('update_groups called') 134 | session = botocore.session.get_session() 135 | session.profile = self.config['master_account_profile'] 136 | iam = session.create_client('iam') 137 | for acct in self.config['assumable_accounts']: 138 | session = botocore.session.get_session() 139 | session.profile = acct['profile'] 140 | stack = rolemodel.stack.Stack( 141 | session, self.config['stack_name'], self.config['assumable_roles'], 142 | self.config['master_account_id']) 143 | if stack.exists(): 144 | self._check_for_groups(iam, acct, stack) 145 | 146 | def delete_groups(self): 147 | LOG.debug('delete_groups called') 148 | session = botocore.session.get_session() 149 | session.profile = self.config['master_account_profile'] 150 | iam = session.create_client('iam') 151 | response = iam.list_groups(PathPrefix=self.Path) 152 | for group in response['Groups']: 153 | group = rolemodel.group.Group( 154 | iam, group['GroupName'], group['Path']) 155 | group.delete() 156 | 157 | def sync_users(self, users): 158 | LOG.debug('sync_users called') 159 | session = botocore.session.get_session() 160 | session.profile = self.config['master_account_profile'] 161 | iam = session.create_client('iam') 162 | for acct in self.config['assumable_accounts']: 163 | if acct['name'] in users: 164 | session = botocore.session.get_session() 165 | session.profile = acct['profile'] 166 | stack = rolemodel.stack.Stack( 167 | session, self.config['stack_name'], self.config['assumable_roles'], 168 | self.config['master_account_id']) 169 | if stack.exists(): 170 | for role in stack.roles(): 171 | role_map = users[acct['name']] 172 | if role.logical_name in role_map: 173 | group_name = self.GroupName.format( 174 | acct=acct['name'], role=role.logical_name) 175 | group = rolemodel.group.Group( 176 | iam, group_name, self.Path) 177 | group.sync_users(role_map[role.logical_name]) 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rolemodel 2 | ========= 3 | 4 | [![Code Health](https://landscape.io/github/scopely-devops/rolemodel/master/landscape.svg)](https://landscape.io/github/scopely-devops/rolemodel/master) 5 | 6 | **Rolemodel** is a command line tool that helps you set up and maintain 7 | cross-account IAM roles for the purpose of using them in the new 8 | [switch role](https://aws.amazon.com/blogs/aws/new-cross-account-access-in-the-aws-management-console/) 9 | capability of the AWS management console. These same cross-account roles 10 | can also be used with the AWSCLI as described 11 | [here](http://lexical.scopely.com/2015/01/09/switching-roles/). 12 | 13 | The main benefit of enabling these cross-account roles is that you only 14 | have to maintain a single set of IAM users in one "master" AWS account. 15 | By controlling which IAM groups these users are members of, you can control 16 | which other accounts they have access to and what privileges they have 17 | in each of those accounts. 18 | 19 | A Little Terminology 20 | -------------------- 21 | 22 | For the purposes of this document, lets define a couple of terms. 23 | 24 | * **Assumable Account** is an AWS account in which IAM roles have been created 25 | for the purpose of allowing cross-account access. You can have any number of 26 | assumable accounts. 27 | * **Master Account** is the account in which you will create and maintain IAM 28 | users. This is the account your users will log into to switch to other 29 | assumable accounts. You can have only one master account. 30 | 31 | What Does rolemodel Do? 32 | ----------------------- 33 | 34 | The ``rolemodel`` tool: 35 | 36 | * Uses CloudFormation to create a consistent set of roles across all assumabe 37 | accounts you specify. 38 | * Creates IAM groups in the master account to control which IAM users in the 39 | master account can assume which roles in each assumable account. If you 40 | have defined four roles and you have 4 assumable accounts, ``rolemodel`` will 41 | create a total of 16 groups in the master account. 42 | * Optionally, ``rolemodel`` can be used to map existing IAM users in the 43 | master account into the appropriate roles for each of the assumable 44 | accounts. It will not create IAM users for you. 45 | 46 | The ``rolemodel`` tool will create all roles and groups initially but can also 47 | be used to update roles over time. If you add more roles or change the 48 | policies of existing roles you can run the ``update`` command and ``rolemodel`` 49 | will take care of the rest. 50 | 51 | What Do I Have To Do? 52 | --------------------- 53 | 54 | As an administrator you are responsible for: 55 | 56 | * Defining the IAM roles and related policies that you want to enable in all of 57 | your assumable accounts. 58 | * Running the ``rolemodel`` tool to create and update the IAM roles when 59 | necessary. 60 | * Manage the membership in the IAM groups created in the master account. By 61 | adding an IAM user to one of the IAM groups you are granting that user the 62 | ability to switch to that account with the privileges granted by the IAM 63 | policies associated with that IAM role. 64 | * Carefully control IAM access in the master account. Any IAM user that can 65 | change group membership in the master account has the ability to elevate any 66 | IAM user in the master account to the most-privileged IAM role in all 67 | assumable accounts. You should control this carefully! 68 | 69 | Getting Started 70 | --------------- 71 | 72 | First, you need to install ``rolemodel``. The easiest way is with pip, 73 | preferably inside a virtualenv. 74 | 75 | $ pip install rolemodel 76 | 77 | You can also clone the 78 | [Github repo](https://github.com/scopely-devops/rolemodel) and then run 79 | ``python setup.py install`` inside the cloned directory. 80 | 81 | ``rolemodel`` is built with [botocore](https://github.com/boto/botocore) and 82 | uses the same credential file as defined by 83 | [AWSCLI](https://github.com/aws/aws-cli). 84 | 85 | The next step is to define your master set of roles in a CloudFormation 86 | template. There is a sample set of roles contained in the file 87 | ``samples/roles.cf`` but you should edit this to suit your needs. Each role 88 | defined in your CloudFormation template will be created in all of the assumable 89 | accounts you specify. 90 | 91 | The next thing you need to do is create a configuration file to tell 92 | ``rolemodel`` about your assumable accounts and other info. There is a sample 93 | YAML config file in ``samples/config.yml``. You need to provide the following 94 | information in the config file. 95 | 96 | * **assumable_roles** should be the path to the CloudFormation template 97 | defining the IAM roles that will be created in each assumable account. 98 | * **stack_name** is the name of the CloudFormation stack that will be created. 99 | * **master_account_id** is the 12-digit ID for the account that will be the 100 | master account. 101 | * **master_account_profile** is the name of the profile within your AWS config 102 | or credential file that contains the credentials for the master account. 103 | * for each assumable account: 104 | * **name** is the symbolic name of the assumable account. This name is used 105 | when constructing the group names in the master account. 106 | * **profile** is the name of the profile within your AWS config or 107 | credential file that contains the credentials for this assumable account. 108 | These credentials must be able to create IAM roles within the assumabl 109 | account. 110 | 111 | Once you have the IAM roles defined and your configuration file created you can 112 | run the ``rolemodel`` command line tool. 113 | 114 | $ rolemodel update 115 | 116 | This will create or update all of the IAM roles defined in your CloudFormation 117 | template in all assumable accounts specified in your configuration file. It 118 | will then create or update all required IAM groups in your master account. You 119 | can run this command multiple times. If no changes have been made in your IAM 120 | roles then nothing will be done by ``rolemodel``. 121 | 122 | If you want to get a list of all roles and all assumable accounts, use: 123 | 124 | $ rolemodel list 125 | 126 | Finally, if you want to delete all IAM roles in all assumable accounts and also 127 | delete all IAM groups in the master account, use: 128 | 129 | $ rolemodel delete 130 | 131 | Managing Users in Master Account 132 | -------------------------------- 133 | 134 | You can do the above steps and then manually manage the process of making IAM 135 | users in the master account members of the appropriate groups to allow them to 136 | assume roles in assumable accounts. However, ``rolemodel`` does provide a 137 | mechanism to support this part of the process as well. 138 | 139 | To take advantage of this feature, you need an additional YAML file that maps 140 | existing IAM users in the master account into the necessary IAM groups. The 141 | structure of this file is shown below. 142 | 143 | --- 144 | acct1: 145 | role1: 146 | - user1 147 | - user2 148 | role2: 149 | - user3 150 | - user4 151 | acct2: 152 | role1: 153 | - user1 154 | - user3 155 | role2: 156 | - user5 157 | - user6 158 | 159 | The main keys in this dictionary are the names of the assumable accounts. 160 | Within each of the accounts are additionaly dictionaries for each of the 161 | assumable roles that are defined. And each role name contains a list of 162 | existing IAM users in the master account that should be allowed to assume that 163 | role. 164 | 165 | Once you have defined this file for your accounts, you can run the command to 166 | sync your groups with this file. 167 | 168 | $ rolemodel sync_users 169 | 170 | When you run this command, ``rolemodel`` will synchronize the users in each IAM 171 | group with the information defined in your user map. That means that some 172 | users may be removed from groups if they no longer appear in the user map for 173 | that group. 174 | 175 | The ``sample`` directory includes an example of a user map file you can edit 176 | for your purposes. 177 | 178 | Groups In Master Account 179 | ------------------------ 180 | 181 | The name of each group created in the master account will be of the form: 182 | 183 | . 184 | 185 | The ``assumable account name`` comes from the name you provide for the 186 | assumable account in the config file. The ``role name`` comes from the name 187 | used for the role in the CloudFormation template. 188 | 189 | In addition, all groups created by ``rolemodel`` will have a path of 190 | /RoleModel/ to help separate them from other resources in IAM. 191 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /sample/roles.cf: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "This stack contains all of the assumable roles. These roles need to be created in each of the accounts into which you wish users to be able to switch. The AssumingAccountID parameter is the 12-digit ID for the account which will be used to actually perform the AssumeRole operation.", 4 | 5 | "Parameters": { 6 | "AssumingAccountID": { 7 | "Type": "String", 8 | "Description" : "The 12-digit ID for the account that will assume the roles", 9 | "MinLength": 12, 10 | "MaxLength": 12, 11 | "AllowedPattern": "[0-9]+", 12 | "ConstraintDescription": "Must contain a 12 digit account ID" 13 | } 14 | }, 15 | 16 | "Resources": { 17 | "AdminRole": { 18 | "Type": "AWS::IAM::Role", 19 | "Properties": { 20 | "AssumeRolePolicyDocument": { 21 | "Version" : "2012-10-17", 22 | "Statement": [ 23 | { 24 | "Effect": "Allow", 25 | "Principal": { 26 | "AWS": 27 | { "Fn::Join": [ 28 | ":", [ 29 | "arn", 30 | "aws", 31 | "iam", 32 | "", 33 | { "Ref": "AssumingAccountID" }, 34 | "root"]] 35 | } 36 | }, 37 | "Action": "sts:AssumeRole", 38 | "Condition": { 39 | "Null": { 40 | "aws:MultiFactorAuthAge": false 41 | } 42 | } 43 | } 44 | ] 45 | } 46 | } 47 | }, 48 | "AdminRolePolicies": { 49 | "Type": "AWS::IAM::Policy", 50 | "Properties": { 51 | "PolicyName": "AdminRolePolicy", 52 | "PolicyDocument": { 53 | "Version" : "2012-10-17", 54 | "Statement": [ 55 | { 56 | "Effect": "Allow", 57 | "Action": "*", 58 | "Resource": "*" 59 | } 60 | ] 61 | }, 62 | "Roles": [ { "Ref": "AdminRole" } ] 63 | } 64 | }, 65 | "SuperUserRole": { 66 | "Type": "AWS::IAM::Role", 67 | "Properties": { 68 | "AssumeRolePolicyDocument": { 69 | "Version" : "2012-10-17", 70 | "Statement": [ 71 | { 72 | "Effect": "Allow", 73 | "Principal": { 74 | "AWS": 75 | { "Fn::Join": [ 76 | ":", [ 77 | "arn", 78 | "aws", 79 | "iam", 80 | "", 81 | { "Ref": "AssumingAccountID" }, 82 | "root"]] 83 | } 84 | }, 85 | "Action": "sts:AssumeRole", 86 | "Condition": { 87 | "Null": { 88 | "aws:MultiFactorAuthAge": false 89 | } 90 | } 91 | } 92 | ] 93 | } 94 | } 95 | }, 96 | "SuperUserRolePolicies": { 97 | "Type": "AWS::IAM::Policy", 98 | "Properties": { 99 | "PolicyName": "SuperUserRolePolicy", 100 | "PolicyDocument": { 101 | "Version": "2012-10-17", 102 | "Statement": [ 103 | { 104 | "Action": [ 105 | "iam:Add*", 106 | "iam:Create*", 107 | "iam:DeactivateMfaDevice", 108 | "iam:Delete*", 109 | "iam:EnableMfaDevice", 110 | "iam:GenerateCredentialReport", 111 | "iam:Put*", 112 | "iam:Remove*", 113 | "iam:ResyncMfaDevice", 114 | "iam:Update*" 115 | ], 116 | "Effect": "Deny", 117 | "Resource": "*" 118 | }, 119 | { 120 | "Effect": "Allow", 121 | "Action": "*", 122 | "Resource": "*" 123 | } 124 | ] 125 | }, 126 | "Roles": [ { "Ref": "SuperUserRole" } ] 127 | } 128 | }, 129 | "ReadOnlyRole": { 130 | "Type": "AWS::IAM::Role", 131 | "Properties": { 132 | "AssumeRolePolicyDocument": { 133 | "Version" : "2012-10-17", 134 | "Statement": [ 135 | { 136 | "Effect": "Allow", 137 | "Principal": { 138 | "AWS": 139 | { "Fn::Join": [ 140 | ":", [ 141 | "arn", 142 | "aws", 143 | "iam", 144 | "", 145 | { "Ref": "AssumingAccountID" }, 146 | "root"]] 147 | } 148 | }, 149 | "Action": "sts:AssumeRole", 150 | "Condition": { 151 | "Null": { 152 | "aws:MultiFactorAuthAge": false 153 | } 154 | } 155 | } 156 | ] 157 | } 158 | } 159 | }, 160 | "ReadOnlyRolePolicies": { 161 | "Type": "AWS::IAM::Policy", 162 | "Properties": { 163 | "PolicyName": "ReadOnlyRolePolicy", 164 | "PolicyDocument": { 165 | "Version": "2012-10-17", 166 | "Statement": [ 167 | { 168 | "Action": [ 169 | "appstream:Get*", 170 | "autoscaling:Describe*", 171 | "cloudformation:DescribeStacks", 172 | "cloudformation:DescribeStackEvents", 173 | "cloudformation:DescribeStackResources", 174 | "cloudformation:GetTemplate", 175 | "cloudformation:List*", 176 | "cloudfront:Get*", 177 | "cloudfront:List*", 178 | "cloudtrail:DescribeTrails", 179 | "cloudtrail:GetTrailStatus", 180 | "cloudwatch:Describe*", 181 | "cloudwatch:Get*", 182 | "cloudwatch:List*", 183 | "directconnect:Describe*", 184 | "dynamodb:GetItem", 185 | "dynamodb:BatchGetItem", 186 | "dynamodb:Query", 187 | "dynamodb:Scan", 188 | "dynamodb:DescribeTable", 189 | "dynamodb:ListTables", 190 | "ec2:Describe*", 191 | "elasticache:Describe*", 192 | "elasticbeanstalk:Check*", 193 | "elasticbeanstalk:Describe*", 194 | "elasticbeanstalk:List*", 195 | "elasticbeanstalk:RequestEnvironmentInfo", 196 | "elasticbeanstalk:RetrieveEnvironmentInfo", 197 | "elasticloadbalancing:Describe*", 198 | "elastictranscoder:Read*", 199 | "elastictranscoder:List*", 200 | "iam:List*", 201 | "iam:Get*", 202 | "opsworks:Describe*", 203 | "opsworks:Get*", 204 | "route53:Get*", 205 | "route53:List*", 206 | "redshift:Describe*", 207 | "redshift:ViewQueriesInConsole", 208 | "rds:Describe*", 209 | "rds:ListTagsForResource", 210 | "s3:Get*", 211 | "s3:List*", 212 | "sdb:GetAttributes", 213 | "sdb:List*", 214 | "sdb:Select*", 215 | "ses:Get*", 216 | "ses:List*", 217 | "sns:Get*", 218 | "sns:List*", 219 | "sqs:GetQueueAttributes", 220 | "sqs:ListQueues", 221 | "sqs:ReceiveMessage", 222 | "storagegateway:List*", 223 | "storagegateway:Describe*" 224 | ], 225 | "Effect": "Allow", 226 | "Resource": "*" 227 | } 228 | ] 229 | }, 230 | "Roles": [ { "Ref": "ReadOnlyRole" } ] 231 | } 232 | }, 233 | "FinanceRole": { 234 | "Type": "AWS::IAM::Role", 235 | "Properties": { 236 | "AssumeRolePolicyDocument": { 237 | "Version" : "2012-10-17", 238 | "Statement": [ 239 | { 240 | "Effect": "Allow", 241 | "Principal": { 242 | "AWS": 243 | { "Fn::Join": [ 244 | ":", [ 245 | "arn", 246 | "aws", 247 | "iam", 248 | "", 249 | { "Ref": "AssumingAccountID" }, 250 | "root"]] 251 | } 252 | }, 253 | "Action": "sts:AssumeRole", 254 | "Condition": { 255 | "Null": { 256 | "aws:MultiFactorAuthAge": false 257 | } 258 | } 259 | } 260 | ] 261 | } 262 | } 263 | }, 264 | "FinanceRolePolicies": { 265 | "Type": "AWS::IAM::Policy", 266 | "Properties": { 267 | "PolicyName": "AdminRolePolicy", 268 | "PolicyDocument": { 269 | "Version" : "2012-10-17", 270 | "Statement": [ 271 | { 272 | "Effect": "Allow", 273 | "Action": [ 274 | "aws-portal:View*"], 275 | "Resource": "*" 276 | }, 277 | { 278 | "Effect": "Deny", 279 | "Action": "aws-portal:*Account", 280 | "Resource": "*" 281 | } 282 | ] 283 | }, 284 | "Roles": [ { "Ref": "FinanceRole" } ] 285 | } 286 | } 287 | } 288 | } 289 | --------------------------------------------------------------------------------