├── awsorgs ├── tools │ ├── __init__.py │ ├── accessrole.py │ └── spec_init.py ├── __init__.py ├── spec_init_data │ ├── config.yaml │ └── spec.d │ │ ├── policy_sets.yaml │ │ ├── users.yaml │ │ ├── groups.yaml │ │ ├── organizational_units.yaml │ │ ├── accounts.yaml │ │ ├── common.yaml │ │ ├── service_control_policies.yaml │ │ ├── local_users.yaml │ │ ├── custom_policies.yaml │ │ └── delegations.yaml ├── data │ └── email_template ├── spec.py ├── validator.py ├── utils.py ├── loginprofile.py ├── reports.py ├── accounts.py └── orgs.py ├── .gitignore ├── docs ├── source │ ├── getting_started.rst │ ├── concepts_and_workflow.rst │ ├── aws_organizations.rst │ ├── usage │ │ ├── awsorgs.rst │ │ ├── awsauth-local-users.rst │ │ ├── awsaccounts.rst │ │ ├── awsloginprofile.rst │ │ ├── awsauth-users.rst │ │ └── awsauth-delegations.rst │ ├── centralized_iam_access.rst │ ├── index.rst │ ├── working_with_spec_files.rst │ ├── example_spec_files.rst │ ├── conf.py │ ├── basic_configuration.rst │ └── testing │ │ ├── test_awsaccounts.rst │ │ ├── test_awsloginprofile.rst │ │ └── test_awsauth.rst ├── Makefile └── project_notes │ └── TODO.txt ├── MANIFEST.in ├── LICENCE.txt ├── setup.py ├── README.rst └── PUBLISHING.rst /awsorgs/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /awsorgs/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.4' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.pyc 4 | *.egg-info 5 | build/ 6 | dist/ 7 | docs/_build 8 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. include:: ../../README.rst 4 | 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | include awsorgs/samples/*.yaml 4 | include awsorgs/data/*.yaml 5 | 6 | -------------------------------------------------------------------------------- /docs/source/concepts_and_workflow.rst: -------------------------------------------------------------------------------- 1 | Concepts and Workflow 2 | ===================== 3 | 4 | Ahh! So much to tell. So little motivation! 5 | -------------------------------------------------------------------------------- /docs/source/aws_organizations.rst: -------------------------------------------------------------------------------- 1 | AWS Organizations 2 | ================= 3 | 4 | .. toctree:: 5 | 6 | usage/awsaccounts 7 | usage/awsorgs 8 | -------------------------------------------------------------------------------- /docs/source/usage/awsorgs.rst: -------------------------------------------------------------------------------- 1 | Organizational Units and Service Control Policies - ``awsorgs`` 2 | =============================================================== 3 | 4 | .. note:: 5 | 6 | Under Custruction 7 | -------------------------------------------------------------------------------- /docs/source/centralized_iam_access.rst: -------------------------------------------------------------------------------- 1 | Centralized IAM Access 2 | ====================== 3 | 4 | .. toctree:: 5 | 6 | usage/awsauth-users 7 | usage/awsauth-delegations 8 | usage/awsloginprofile 9 | usage/awsauth-local-users 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. aws-orgs documentation master file, created by 2 | sphinx-quickstart on Tue Jul 23 15:57:31 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to aws-orgs's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | getting_started 14 | basic_configuration 15 | concepts_and_workflow 16 | working_with_spec_files 17 | aws_organizations 18 | centralized_iam_access 19 | 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :hidden: 24 | 25 | testing/test_awsaccounts 26 | testing/test_awsauth 27 | testing/test_awsloginprofile 28 | 29 | 30 | :ref:`search` 31 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/config.yaml: -------------------------------------------------------------------------------- 1 | # AWSORGS Configuration Parameters 2 | 3 | # Path to yaml spec files directory. Any yaml files under this dirctory 4 | # (recursive) are parsed as spec files. 5 | spec_dir: ~/.awsorgs/spec.d 6 | 7 | # An AWS role name which permits cross account access to all accounts. 8 | org_access_role: awsauth/OrgAdmin 9 | 10 | # Use an alternate role from master account when setting up a new account. 11 | #org_access_role: OrganizationAccountAccessRole 12 | 13 | # AWS account Id for the Organization master account. This must be in quotes. 14 | master_account_id: '121212121212' 15 | 16 | # AWS account Id for the Central Auth account. This must be in quotes. 17 | # This Central Auth account can be the same as the Master account. 18 | auth_account_id: '343434343434' 19 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/policy_sets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Policy Set Specification 3 | # 4 | # A Policy Set is a list of IAM policies, either AWS managed or customer 5 | # managed, which taken in composit, define the permissions available to 6 | # a particular job function, such as "Developer" of "SecurityAuditor". 7 | # 8 | # Each policy set spec has the following attributes: 9 | # Name (str): the policy set name 10 | # Descriptsion (str): describes the scope of the policy set 11 | # Tags (list(dict)): list of tags to apply to delegation roles made from 12 | # this policy set 13 | # Policies (list(str)): list of IAM policy names 14 | 15 | policy_sets: 16 | - Name: Developer 17 | Description: Access for application developers. 18 | Tags: 19 | - Key: jobfunctionrole 20 | Value: True 21 | Policies: 22 | - PowerUserAccess 23 | - IAMReadOnlyAccess 24 | - Name: TesterPolicySet 25 | Description: Access for testers 26 | Tags: 27 | - Key: jobfunctionrole 28 | Value: True 29 | Policies: 30 | - ReadOnlyAccess 31 | - ReadS3Bucket 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/source/working_with_spec_files.rst: -------------------------------------------------------------------------------- 1 | Working with Spec Files 2 | ======================= 3 | 4 | .. note:: 5 | 6 | Work in Progress 7 | 8 | 9 | Shared Specs 10 | ************ 11 | 12 | :ref:`example_spec_files:common.yaml` 13 | Top level spec attributes common to all tools. 14 | 15 | 16 | :ref:`example_spec_files:teams.yaml` 17 | these are used as attributes in other spec objects: 18 | 19 | - accounts 20 | - users 21 | - local_users 22 | 23 | 24 | 25 | ``awsorgs`` 26 | *********** 27 | 28 | :ref:`example_spec_files:organizational_units.yaml` 29 | 30 | :ref:`example_spec_files:service_control_policies.yaml` 31 | 32 | 33 | 34 | ``awsaccounts`` 35 | *************** 36 | 37 | :ref:`example_spec_files:accounts.yaml` 38 | 39 | 40 | 41 | ``awsauth`` 42 | *********** 43 | 44 | :ref:`example_spec_files:users.yaml` 45 | 46 | :ref:`example_spec_files:groups.yaml` 47 | 48 | :ref:`example_spec_files:delegations.yaml` 49 | 50 | :ref:`example_spec_files:policy_sets.yaml` 51 | 52 | :ref:`example_spec_files:custom_policies.yaml` 53 | 54 | :ref:`example_spec_files:local_users.yaml` 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/users.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Users Specification 3 | # 4 | # List of IAM users managed within the Central Auth account. 5 | # 6 | # Each user has the following attributes all of type 'str': 7 | # Name (str): The name of the user - required. 8 | # Ensure ('present'[default]|'absent'): 9 | # Setting to 'absent' will cause the user to be deleted. 10 | # CN (str): ActiveDirectory 'cn' attribute for this user. 11 | # i.e. 12 | # Email (str): The email address with which the user can be contacted. 13 | # Should match the ActiveDirectory 'mail' attribute. 14 | # RequestId (str): Ticketing system tracking number of a new user request. 15 | 16 | users: 17 | - Name: ashley 18 | Ensure: present 19 | CN: Ashley Gould 20 | Email: ashley@example.com 21 | RequestId: RIT0012340 22 | - Name: kalila 23 | Ensure: present 24 | CN: Kalila Bidpai 25 | Email: kalila@example.com 26 | RequestId: RIT0012341 27 | - Name: dimna 28 | Ensure: present 29 | CN: Dimna Bidpai 30 | Email: dimna@example.com 31 | RequestId: RIT0012342 32 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/groups.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Group Specification 3 | # 4 | # List of IAM group resources managed within the Central Auth account. 5 | # 6 | # Each group has the following attributes: 7 | # Name (str): The group name. 8 | # Ensure (str): One of 'present' (default) or 'absent'. Setting to 9 | # 'absent' will cause the group to be deleted, but 10 | # only if the group contains no users. 11 | # Members (list(str), 'ALL'): 12 | # List of IAM users who are members of this group. 13 | # If set to 'ALL', all managed users in the Central 14 | # Auth account become members. 15 | # ExcludeMembers (list(str)): 16 | # If 'Members' attribute is set to 'ALL', any users 17 | # listed in 'ExludeMembers' are excluded from the group. 18 | # Policies (list(str)): 19 | # List of IAM policies to attach to this group. 20 | 21 | groups: 22 | - Name: all-users 23 | Members: ALL 24 | Policies: 25 | - UserSelfService 26 | - Name: admins 27 | Members: 28 | - ashley 29 | - Name: devops 30 | Members: 31 | - kalila 32 | - dimna 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/organizational_units.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Organizational Unit Specification. 3 | # 4 | # This specification maps the Organization's structure and assigns policies and 5 | # accounts to organizational units. 6 | # 7 | # Each organizational_unit spec (OU) has the following attributes: 8 | # Name (str): The name of the OU (required) 9 | # Ensure (str): One of 'present' (default) or 'absent'. Setting to 10 | # 'absent' will cause the OU to be deleted but 11 | # only if no accounts are still assigned to the OU. 12 | # Accounts (list(str)): 13 | # List of account names assigned to this OU. 14 | # SC_Policies (list(str)): 15 | # List of Service Control Policies attached to this OU. 16 | # Child_OU (list(organizational_unit)): 17 | # List of child Organizational Units (recursive structure). 18 | 19 | organizational_units: 20 | # the root OU must be defined 21 | - Name: root 22 | Accounts: 23 | - master-account 24 | - central-auth 25 | Policies: 26 | Child_OU: 27 | - Name: applications 28 | Accounts: 29 | - dev1 30 | SC_Policies: 31 | - LimitAWSRegions 32 | 33 | 34 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/accounts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # List of managed AWS accounts in the Organization. 3 | # 4 | # Each account spec the following attributes: 5 | # Name (str): The name of the account 6 | # Email (str): The email address used when creating a new account. This 7 | # address must be unique in all AWS. If omitted, we combine 8 | # the account name and the default_email_domain. 9 | # Alias (str): String to use for the account alias. Defaults to 'Name' in 10 | # lower case. 11 | # Tags (dict): Tags to apply to the AWS account. The tag value can have 12 | # up to 256 characters. 13 | # Valid characters: a-z, A-Z, 0-9, and . : + = @ _ / - (hyphen) 14 | 15 | accounts: 16 | - Name: master-account 17 | Email: master-account@example.com 18 | Alias: master 19 | Tags: 20 | Owner: Yegamani Kumar 21 | Application: infrastructure 22 | Environment: production 23 | - Name: central-auth 24 | Email: central-auth@example.com 25 | Alias: auth 26 | Tags: 27 | Owner: Yegamani Kumar 28 | Application: infrastructure 29 | Environment: production 30 | - Name: dev1 31 | Email: dev1@example.com 32 | Alias: dev1 33 | Tags: 34 | Owner: Candy Jong 35 | Application: fistua 36 | Environment: development 37 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/common.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # aws-orgs installation must be greater than or eqaul to this version number. 4 | minimum_version: '0.3.4' 5 | 6 | # AWSORGS configuration parameters common to all operations. 7 | 8 | # AWS account Id for the Organization master account. This must be in quotes. 9 | master_account_id: '121212121212' 10 | 11 | # AWS account Id for the Central Auth account. This must be in quotes. 12 | auth_account_id: '343434343434' 13 | 14 | # Email domain to use for account creation if the accounts['Email'] field 15 | # is not explicitly specified. 16 | default_domain: example.com 17 | 18 | # Default Organizational Unit. Any accounts in the Organization not 19 | # explicitly assigned to an Organizational Unit are placed here. 20 | default_ou: root 21 | 22 | # Default Organization Service Control Policy. This is managed by AWS and 23 | # should not be modified or deleted. This is attached to all Organizational 24 | # Units. 25 | default_sc_policy: FullAWSAccess 26 | 27 | # This string is prepended to all IAM resource 'path' attributes. 28 | default_path: awsauth 29 | 30 | # Default SMTP email server to use when sending email messages. 31 | default_smtp_server: smtp.example.com 32 | 33 | # These contacts are references when generating email messages. 34 | org_admin_email: awsadmins@example.com 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/service_control_policies.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Service Control Policy Specification 3 | # 4 | # Defines custom Service Control Policies which can then be attached 5 | # to Organizational Units. 6 | # 7 | # Each service control policy spec (SCP) has the following attributes: 8 | # Name (str): The name of the SCP. 9 | # Ensure (str): One of 'present' (default) or 'absent'. Setting 10 | # to 'absent' will cause the SCP to be deleted, but 11 | # only if it not attached to any Organizational Unit. 12 | # Description (str): The policy description. 13 | # Statement (list(dict)): 14 | # List of IAM policy statements applied to the SCP. 15 | 16 | sc_policies: 17 | - PolicyName: LimitAWSRegions 18 | Ensure: present 19 | Description: Limit the AWS regions where users can deploy resources 20 | Statement: 21 | - Sid: DenyAllRegionsOutsideUS 22 | Effect: Deny 23 | NotAction: 24 | - iam:* 25 | - organizations:* 26 | - route53:* 27 | - budgets:* 28 | - waf:* 29 | - cloudfront:* 30 | - globalaccelerator:* 31 | - importexport:* 32 | - support:* 33 | Resource: '*' 34 | Condition: 35 | StringNotEquals: 36 | aws:RequestedRegion: 37 | - us-east-1 38 | - us-east-2 39 | - us-west-1 40 | - us-west-2 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/source/example_spec_files.rst: -------------------------------------------------------------------------------- 1 | Example Spec Files 2 | ================== 3 | 4 | shared 5 | 6 | common.yaml 7 | ----------- 8 | 9 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/common.yaml 10 | 11 | teams.yaml 12 | ---------- 13 | 14 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/teams.yaml 15 | 16 | 17 | ---- 18 | 19 | ``awsorgs`` 20 | 21 | 22 | organizational_units.yaml 23 | ------------------------- 24 | 25 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/organizational_units.yaml 26 | 27 | 28 | service_control_policies.yaml 29 | ----------------------------- 30 | 31 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/service_control_policies.yaml 32 | 33 | 34 | ---- 35 | 36 | ``awsaccounts`` 37 | 38 | 39 | accounts.yaml 40 | ------------- 41 | 42 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/accounts.yaml 43 | 44 | 45 | ---- 46 | 47 | ``awsauth`` 48 | 49 | 50 | users.yaml 51 | ---------- 52 | 53 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/users.yaml 54 | 55 | groups.yaml 56 | ----------- 57 | 58 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/groups.yaml 59 | 60 | 61 | delegations.yaml 62 | ---------------- 63 | 64 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/delegations.yaml 65 | 66 | 67 | policy_sets.yaml 68 | ---------------- 69 | 70 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/policy_sets.yaml 71 | 72 | 73 | custom_policies.yaml 74 | -------------------- 75 | 76 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/custom_policies.yaml 77 | 78 | 79 | local_users.yaml 80 | ---------------- 81 | 82 | .. literalinclude:: ../../awsorgs/spec_init_data/spec.d/local_users.yaml 83 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """aws-orgs setup""" 2 | 3 | from awsorgs import __version__ 4 | from setuptools import setup, find_packages 5 | from codecs import open 6 | from os import path 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name='aws-orgs', 14 | version=__version__, 15 | description='Tools to manage AWS Organizations', 16 | long_description=long_description, 17 | url='https://github.com/ucopacme/aws-orgs', 18 | author='Ashley Gould', 19 | author_email='agould@ucop.edu', 20 | license='MIT', 21 | classifiers=[ 22 | 'Development Status :: 4 - Beta', 23 | 'Intended Audience :: Developers', 24 | 'Intended Audience :: System Administrators', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 3.6', 27 | 'Programming Language :: Python :: 3.7', 28 | ], 29 | keywords='aws organizations', 30 | packages=find_packages(exclude=['scratch', 'notes']), 31 | install_requires=[ 32 | 'boto3', 33 | 'docopt', 34 | 'PyYAML', 35 | 'passwordgenerator', 36 | 'cerberus', 37 | ], 38 | package_data={ 39 | 'awsorgs': [ 40 | 'data/*', 41 | 'spec_init_data/*', 42 | 'spec_init_data/spec.d/*', 43 | ], 44 | }, 45 | entry_points={ 46 | 'console_scripts': [ 47 | 'awsorgs=awsorgs.orgs:main', 48 | 'awsaccounts=awsorgs.accounts:main', 49 | 'awsauth=awsorgs.auth:main', 50 | 'awsloginprofile=awsorgs.loginprofile:main', 51 | 'awsorgs-accessrole=awsorgs.tools.accessrole:main', 52 | 'awsorgs-spec-init=awsorgs.tools.spec_init:main', 53 | ], 54 | }, 55 | 56 | ) 57 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/local_users.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Local User Specification 3 | # 4 | # IAM user resources can be deployed into managed accounts. Local users 5 | # are typically accociated with a service such as SES or S3. Such users 6 | # do not have a login profile. 7 | # 8 | # Each local user spce has the following attributes: 9 | # Name (str): The name of local IAM user. 10 | # Ensure ('present'[default]|'absent'): 11 | # Ensures whether the local user exists or not. 12 | # ContactEmail (str): 13 | # The email address with which the user can be contacted. 14 | # Should match the ActiveDirectory 'mail' attribute. 15 | # RequestId (str): Ticketing system tracking number of a new user request. 16 | # 17 | # Description (str):A decription applied to the local IAM user. 18 | # Service (str): Name of the AWS service this user interacts with. This 19 | # is used in the IAM resource path. 20 | # Account (list(str), 'ALL'): 21 | # List of accounts in which the user is deployed. 22 | # If set to 'ALL', the local user will be created in 23 | # all accounts in the Organization. 24 | # ExcludeAccounts (list(str)): 25 | # If 'TrustingAccount' attribute is set to 'ALL', 26 | # any accounts listed in 'ExludeAccounts' are 27 | # excluded from the delegation. 28 | # Policies (list(str)): 29 | # List of IAM policies to attach to the local user. 30 | 31 | local_users: 32 | - Name: local-service-user 33 | Ensure: present 34 | ContactEmail: test@ucop.edu 35 | RequestId: RIT00000123 36 | Description: Local service user 37 | Service: ses 38 | Account: 39 | - dev1 40 | Policies: 41 | - AmazonSesSendingAccess 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /awsorgs/tools/accessrole.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Generate default org access role in an invited account. 3 | Run this with IAM credentials for invited account. 4 | 5 | Creates role 'OrganizationAccountAccessRole' allowing users in 6 | Org Master account 'AdministratorAccess' in invited account. 7 | 8 | Usage: 9 | awsorgs-accessrole --master_id ID [--exec] 10 | awsorgs-accessrole --help 11 | awsorgs-accessrole --version 12 | 13 | Options: 14 | -m, --master_id ID Master Account ID 15 | -h, --help Show this help message and exit. 16 | -V, --version Display version info and exit. 17 | """ 18 | 19 | import json 20 | 21 | import boto3 22 | from docopt import docopt 23 | 24 | import awsorgs 25 | from awsorgs.utils import lookup 26 | 27 | ROLENAME = 'OrganizationAccountAccessRole' 28 | DESCRIPTION = 'Organization Access Role' 29 | POLICYNAME = 'AdministratorAccess' 30 | 31 | 32 | def main(): 33 | args = docopt(__doc__, version=awsorgs.__version__) 34 | iam_client = boto3.client('iam') 35 | # assemble assume-role policy statement 36 | principal = "arn:aws:iam::%s:root" % args['--master_id'] 37 | statement = dict( 38 | Effect='Allow', 39 | Principal=dict(AWS=principal), 40 | Action='sts:AssumeRole') 41 | policy_doc = json.dumps(dict( 42 | Version='2012-10-17', Statement=[statement])) 43 | # create role 44 | print("Creating role %s" % ROLENAME) 45 | if args['--exec']: 46 | iam_client.create_role( 47 | Description=DESCRIPTION, 48 | RoleName=ROLENAME, 49 | AssumeRolePolicyDocument=policy_doc) 50 | # attach policy to new role 51 | iam_resource = boto3.resource('iam') 52 | aws_policies = iam_client.list_policies( 53 | Scope='AWS', MaxItems=500)['Policies'] 54 | policy_arn = lookup(aws_policies, 'PolicyName', POLICYNAME, 'Arn') 55 | role = iam_resource.Role(ROLENAME) 56 | try: 57 | role.load() 58 | except Exception: 59 | pass 60 | else: 61 | print("Attaching policy %s to %s" % (POLICYNAME, ROLENAME)) 62 | if args['--exec'] and policy_arn: 63 | role.attach_policy(PolicyArn=policy_arn) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /awsorgs/tools/spec_init.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Install initial aws-orgs config and spec files into user's home directory 4 | 5 | Usage: 6 | awsorgs-spec-init [-h | --help] [--config File] [--spec-dir PATH] 7 | 8 | Options: 9 | -h, --help Show this message and exit. 10 | --config FILE Where to install AWS Org config file 11 | [Default: ~/.awsorgs/config.yaml]. 12 | --spec-dir PATH Where to install AWS Org specification files 13 | [Default: ~/.awsorgs/spec.d]. 14 | ''' 15 | 16 | 17 | import os 18 | import sys 19 | import shutil 20 | import pkg_resources 21 | from docopt import docopt 22 | 23 | 24 | def main(): 25 | args = docopt(__doc__) 26 | errors = [] 27 | source_dir = os.path.abspath( 28 | pkg_resources.resource_filename(__name__, '../spec_init_data') 29 | ) 30 | source_config_file = os.path.join(source_dir, 'config.yaml') 31 | source_spec_dir = os.path.join(source_dir, 'spec.d') 32 | target_config_file = os.path.expanduser(args['--config']) 33 | target_config_dir = os.path.dirname(target_config_file) 34 | target_spec_dir = os.path.expanduser(args['--spec-dir']) 35 | 36 | if os.path.exists(target_config_file): 37 | errors.append( 38 | "Config file '{}' exists. " 39 | "Refusing to overwrite.".format(target_config_file) 40 | ) 41 | else: 42 | try: 43 | os.makedirs(target_config_dir) 44 | except OSError: 45 | if not os.path.isdir(target_config_dir): 46 | raise 47 | shutil.copy(source_config_file, target_config_file) 48 | 49 | try: 50 | os.makedirs(target_spec_dir) 51 | except OSError: 52 | if not os.path.isdir(target_spec_dir): 53 | raise 54 | if os.listdir(target_spec_dir): 55 | errors.append( 56 | "Spec directory '{}' exists and is not empty. " 57 | "Refusing to overwrite.".format(target_spec_dir) 58 | ) 59 | else: 60 | for file in os.listdir(source_spec_dir): 61 | shutil.copy( 62 | os.path.join(source_spec_dir, file), 63 | target_spec_dir, 64 | ) 65 | 66 | if errors: 67 | sys.exit('\n'.join(errors)) 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'aws-orgs' 21 | copyright = '2019, Ashley Gould' 22 | author = 'Ashley Gould' 23 | 24 | # The short X.Y version 25 | version = '0.3' 26 | 27 | # The full version, including alpha/beta/rc tags 28 | #release = '0.3.0' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # fix RTD confusion - 'contents.rst not found' 34 | master_doc = 'index' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autosectionlabel' 41 | ] 42 | autosectionlabel_prefix_document = True 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = 'alabaster' 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ['_static'] 64 | -------------------------------------------------------------------------------- /awsorgs/data/email_template: -------------------------------------------------------------------------------- 1 | Dear User, 2 | 3 | You have been granted access to our central AWS authentication 4 | account. From here you can assume designated roles into other AWS 5 | accounts in our Organization. 6 | 7 | You must complete the following tasks to configure your access: 8 | 9 | 10 | 1) Use the credentials below to log into the AWS console. You will be 11 | required to change your password as you log in. The rules for good 12 | passwords are as follows: 13 | 14 | - Minimum password length: 14 15 | - Require at least one uppercase character from Latin alphabet. (A-Z) 16 | - Require at least one lowercase character from Latin alphabet. (a-z) 17 | - Require at least one symbol. (!@#$$%^&*()_+-=[]{}|') 18 | - Require at least one number. (0-9) 19 | 20 | IMPORTANT: your one time password will expire after 24 hours. 21 | 22 | IAM User Name: $user_name 23 | One Time Password: $onetimepw 24 | Login URL: https://${trusted_account}.signin.aws.amazon.com/console 25 | 26 | 27 | 2) Set up your 'Virtual MFA Device' in the AWS console. 28 | 29 | Instructions: 30 | IMPORTANT: The name of the mfa device MUST match your username. 31 | Example MFA name: palmueti 32 | 33 | http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable_virtual.html#enable-virt-mfa-for-iam-user 34 | 35 | You can use either Duo or Google Authenticator as your virtual MFA device. 36 | 37 | Instructions for installing Google Authenticator: 38 | https://support.google.com/accounts/answer/1066447?co=GENIE.Platform%3DiOS&hl=en&oco=0 39 | 40 | 41 | 3) Log out and log back in again. You will be queried for your 6 digit 42 | token code. 43 | 44 | 45 | 4) Verify you can switch role into any accounts where you have cross 46 | account access. From the delegation information listed below supply 47 | the account_alias or account_id in the 'Account' field. Supply the 48 | role_name in the 'Role' field. 49 | 50 | Instructions: 51 | https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-console.html?icmpid=docs_iam_console 52 | 53 | 54 | 5) (optional) Set up 'AWS Access Keys' for your IAM user. 55 | 56 | Instructions: 57 | http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey 58 | 59 | 60 | Your IAM user has been delegated cross account access to the following 61 | accounts. 62 | 63 | $delegations 64 | 65 | -------------------------------------------------------------------------------- /docs/source/basic_configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration Basics 2 | ==================== 3 | 4 | There are two aspects os ``aws-orgs`` configuration: 5 | 6 | 1. per-user cli configuration - `config.yaml`_ 7 | #. site wide resource specifcation - `Spec files`_ 8 | 9 | The ``awsorgs-spec-init`` tool is provided to `Bootstrap your initial configuration`_. 10 | 11 | 12 | config.yaml 13 | ----------- 14 | 15 | Most CLI commands make use of a per-user config file for basic paramaters. The 16 | default location is ``~/.awsorgs/config.yaml``. This file supplies the values 17 | for required cli option parameters:: 18 | 19 | --spec-dir 20 | --master-account-id 21 | --auth-account-id 22 | --org-access-role 23 | 24 | Example: 25 | 26 | .. literalinclude:: ../../awsorgs/spec_init_data/config.yaml 27 | 28 | 29 | Copy this file to your home directory (or run ``awsorgs-spec-init``) and edit 30 | parameter values to suit your AWS Organization. 31 | 32 | 33 | Spec files 34 | ---------- 35 | 36 | AWS-ORGS makes use of a complex of YAML formatted resource specification files. 37 | The spec files are used by aws-orgs commands to deploy and maintain AWS accounts and 38 | IAM resources across your Organization. 39 | 40 | A set of example spec files gets installed on your system when you run 41 | ``awsorg-spec-init``. Each or the example spec files contains complete 42 | documentation of spec attributes. Edit these to suit your AWS Organization. 43 | 44 | .. toctree:: 45 | 46 | example_spec_files 47 | 48 | 49 | The default spec directory is ``~/.awsorgs/spec.d``. If you choose a non-default 50 | location, be sure to update the ``spec_dir`` parameter in your ``config.yaml``. 51 | 52 | .. note:: 53 | 54 | Keep your spec files under version control! 55 | 56 | 57 | 58 | Bootstrap your initial configuration 59 | ------------------------------------ 60 | 61 | ``aws-orgs`` provides a helper script ``awsorgs-spec-init``. This script generates 62 | an initial ``config.yaml`` and a full set of example spec files. By default 63 | these are installed under ``~/.awsorgs``:: 64 | 65 | > awsorgs-spec-init 66 | find ~/.awsorgs 67 | ~/.awsorgs/config.yaml 68 | ~/.awsorgs/spec.d/accounts.yaml 69 | ~/.awsorgs/spec.d/common.yaml 70 | ~/.awsorgs/spec.d/custom_policies.yaml 71 | ~/.awsorgs/spec.d/delegations.yaml 72 | ~/.awsorgs/spec.d/groups.yaml 73 | ~/.awsorgs/spec.d/local_users.yaml 74 | ~/.awsorgs/spec.d/orgs.yaml 75 | ~/.awsorgs/spec.d/policy-sets.yaml 76 | ~/.awsorgs/spec.d/service_control_policies.yaml 77 | ~/.awsorgs/spec.d/teams.yaml 78 | ~/.awsorgs/spec.d/users.yaml 79 | 80 | Run ``awsorgs-spec-init --help`` for options on how to install to alternate locations. 81 | -------------------------------------------------------------------------------- /docs/source/usage/awsauth-local-users.rst: -------------------------------------------------------------------------------- 1 | IAM Service Users - ``awsauth local-users`` 2 | =========================================== 3 | 4 | Prerequisites: 5 | 6 | - custom policy to attach to service user. 7 | 8 | 9 | Commands used: 10 | 11 | - git diff 12 | - awsauth local-users 13 | - awsauth local-users --exec 14 | - awsauth report --users 15 | 16 | 17 | Spec files impacted: 18 | 19 | - :ref:`example_spec_files:local_users.yaml` 20 | - :ref:`example_spec_files:custom_policies.yaml` 21 | 22 | 23 | Actions Summary: 24 | 25 | - `Create an IAM Service user` 26 | - `Modify attached custom policy` 27 | - `Delete an IAM Service user` 28 | 29 | 30 | Create an IAM Service user 31 | ************************** 32 | 33 | Edit the following files: 34 | 35 | - local_users.yaml 36 | 37 | Example Diff:: 38 | 39 | ~/.awsorgs/spec.d> git diff 40 | diff --git a/local_users.yaml b/local_users.yaml 41 | index 2e2521b..6858c36 100644 42 | --- a/local_users.yaml 43 | +++ b/local_users.yaml 44 | @@ -29,6 +29,15 @@ local_users: 45 | Account: All 46 | Policies: 47 | - ReadOnlyAccess 48 | +- Name: ses-smtp-user-myapp 49 | + Description: Local service user for SES SMTP access 50 | + Team: myapp 51 | + Path: service 52 | + Account: 53 | + - myapp-build 54 | + - myapp-prod 55 | + Policies: 56 | + - AmazonSesSendingAccess 57 | 58 | 59 | Review proposed changes in ``dry-run`` mode:: 60 | 61 | $ awsauth local-users 62 | 63 | Implement and review changes:: 64 | 65 | $ awsauth local-users --exec 66 | $ awsauth report --users 67 | 68 | 69 | Modify attached custom policy 70 | ***************************** 71 | 72 | See :ref:`modify_custom_policy` 73 | 74 | 75 | 76 | Delete an IAM Service user 77 | ************************** 78 | 79 | Files to edit: 80 | 81 | - local-users.yaml 82 | 83 | To delete IAM entities we must set attribute ``Ensure: absent`` to associated spec. 84 | 85 | Example diff:: 86 | 87 | ~/.awsorgs/spec.d> git diff 88 | diff --git a/local-users.yaml b/local-users.yaml 89 | index 6858c36..3c89841 100644 90 | --- a/local_users.yaml 91 | +++ b/local_users.yaml 92 | @@ -30,6 +30,7 @@ local_users: 93 | Policies: 94 | - ReadOnlyAccess 95 | - Name: ses-smtp-user-myapp 96 | + Ensure: absent 97 | Description: Local service user for SES SMTP access 98 | Team: myapp 99 | Path: service 100 | 101 | 102 | 103 | Review proposed changes in ``dry-run`` mode:: 104 | 105 | $ awsauth local-users 106 | 107 | Implement and review changes:: 108 | 109 | $ awsauth local-users --exec 110 | $ awsauth report --users 111 | 112 | 113 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/custom_policies.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # IAM Custom Policy Specification 3 | # 4 | # List of IAM policy definitions. Custom policies are created in accounts in 5 | # which the policy is attached to least one group or delegation role. 6 | # 7 | # 8 | # Each custom policy spec (SCP) has the following attributes: 9 | # Name (str): The name of the SCP. 10 | # Description (str): The policy description. 11 | # Statement (list(dict)): 12 | # List of IAM policy statements applied to the SCP. 13 | 14 | custom_policies: 15 | - PolicyName: UserSelfService 16 | Description: Allow users to manage thier own account and credentials 17 | Statement: 18 | - Sid: AllowAllUsersToListAccounts 19 | Effect: Allow 20 | Action: 21 | - iam:ListAccountAliases 22 | - iam:ListUsers 23 | - iam:ListUserPolicies 24 | - iam:ListAttachedUserPolicies 25 | - iam:GetAccountSummary 26 | - iam:ListGroups 27 | - iam:ListGroupPolicies 28 | - iam:ListAttachedGroupPolicies 29 | - iam:GetGroup 30 | - iam:GetGroupPolicy 31 | - iam:ListMFADevices 32 | Resource: "*" 33 | - Sid: AllowIndividualUserToSeeAndManageTheirOwnAccountInformation 34 | Effect: Allow 35 | Action: 36 | - iam:ListGroupsForUser 37 | - iam:ChangePassword 38 | - iam:CreateAccessKey 39 | - iam:CreateLoginProfile 40 | - iam:DeleteAccessKey 41 | - iam:DeleteLoginProfile 42 | - iam:GetAccountPasswordPolicy 43 | - iam:GetLoginProfile 44 | - iam:ListAccessKeys 45 | - iam:UpdateAccessKey 46 | - iam:UpdateLoginProfile 47 | - iam:ListSigningCertificates 48 | - iam:DeleteSigningCertificate 49 | - iam:UpdateSigningCertificate 50 | - iam:UploadSigningCertificate 51 | - iam:ListSSHPublicKeys 52 | - iam:GetSSHPublicKey 53 | - iam:DeleteSSHPublicKey 54 | - iam:UpdateSSHPublicKey 55 | - iam:UploadSSHPublicKey 56 | Resource: arn:aws:iam::*:user/*/${aws:username} 57 | - Sid: AllowIndividualUserToListTheirOwnMFA 58 | Effect: Allow 59 | Action: 60 | - iam:ListVirtualMFADevices 61 | - iam:ListMFADevices 62 | Resource: 63 | - arn:aws:iam::*:mfa/* 64 | - arn:aws:iam::*:user/*/${aws:username} 65 | - Sid: AllowIndividualUserToManageTheirOwnMFA 66 | Effect: Allow 67 | Action: 68 | - iam:CreateVirtualMFADevice 69 | - iam:DeactivateMFADevice 70 | - iam:DeleteVirtualMFADevice 71 | - iam:RequestSmsMfaRegistration 72 | - iam:FinalizeSmsMfaRegistration 73 | - iam:EnableMFADevice 74 | - iam:ResyncMFADevice 75 | Resource: 76 | - arn:aws:iam::*:mfa/${aws:username} 77 | - arn:aws:iam::*:user/*/${aws:username} 78 | - Sid: BlockAnyAccessOtherThanAboveUnlessSignedInWithMFA 79 | Effect: Deny 80 | NotAction: 81 | - iam:* 82 | Resource: "*" 83 | Condition: 84 | BoolIfExists: 85 | aws:MultiFactorAuthPresent: 'false' 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /docs/source/testing/test_awsaccounts.rst: -------------------------------------------------------------------------------- 1 | Functional tests for awsorgs/awsaccounts 2 | ======================================== 3 | 4 | Prerequisites: 5 | 6 | - admin access to auth account 7 | - spec file setup 8 | 9 | - ~/.awsorgs/config.yaml for awsorgs configuration parameters 10 | - spec files in spec_dir directory which is defined in config.yaml 11 | - create at least one satelite account (see awsaccounts) 12 | 13 | 14 | 15 | AWS account alias 16 | ----------------- 17 | 18 | Commands used: 19 | 20 | - awsaccounts alias 21 | - awsaccounts alias --exec 22 | - awsaccounts report 23 | 24 | 25 | spec files impacted: 26 | 27 | - ~/.awsorgs/config.yaml 28 | - spec_dir/account-spec.yml 29 | 30 | 31 | Actions Summary: 32 | 33 | - Define org_access_role in ~/.awsorgs/config.yaml 34 | - Define spec_dir/accoount-spec.yml 35 | - awsaccoutns alias 36 | - awsaccoutns alias --exec 37 | - awsaccoutns report 38 | 39 | 40 | 41 | Define org_access_role in ~/.awsorgs/config.yaml 42 | ************************************************ 43 | 44 | Edit ~/.awsorgs/config.yaml :: 45 | 46 | org_access_role: OrganizationAccountAccessRole 47 | 48 | 49 | 50 | Show current awsaccoutns alias 51 | ****************************** 52 | 53 | Run 'awsacccouns report' :: 54 | 55 | (py36) [jhsu@scrappy-aws ~]$ awsaccounts report 56 | 57 | _______________________ 58 | Active Accounts in Org: 59 | 60 | Name: Alias Id: Email: 61 | account-abcaws1 acct-abcaws1 123456789011 mail1@yahoo.com 62 | account-abcaws2 acct-abcaws2 123456789011 mail2@yahoo.com 63 | account-abcaws3 acct-abcaws3 123456789011 mail3@yahoo.com 64 | 65 | 66 | 67 | Edit spec_dir/accoount-spec.yml 68 | ******************************* 69 | 70 | Change account-abcaws3 alias from acct-abcaws3 to accnt-abcaws3:: 71 | 72 | - Name: account-abcaws3 73 | Team: team-abcaws3 74 | Alias: accnt-abcaws3 75 | Email: mail3@yahoo.com 76 | 77 | 78 | 79 | Dryrun awsaccounts alias 80 | ************************ 81 | 82 | Run 'awsaccount alias' :: 83 | 84 | (py36) [jhsu@scrappy-aws doc]$ awsaccounts alias 85 | 86 | [dryrun] awsorgs.utils: INFO resetting account alias for account 'account-abcaws3' to 'accnt-abcaws3'; previous alias was 'acct-abcaws3' 87 | 88 | 89 | 90 | Exec awsaccounts alias 91 | ********************** 92 | 93 | Run 'awsaccount alias --exec' :: 94 | 95 | (py36) [jhsu@scrappy-aws doc]$ awsaccounts alias --exec 96 | 97 | awsorgs.utils: INFO resetting account alias for account 'account-abcaws3' to 'accnt-abcaws3'; previous alias was 'acct-abcaws3' 98 | 99 | 100 | 101 | awsaccounts report 102 | ****************** 103 | 104 | Run 'awsaccount report' :: 105 | 106 | (py36) [jhsu@scrappy-aws doc]$ awsaccounts report 107 | 108 | 109 | _______________________ 110 | Active Accounts in Org: 111 | 112 | Name: Alias Id: Email: 113 | account-abcaws1 acct-abcaws1 123456789011 mail1@yahoo.com 114 | account-abcaws2 acct-abcaws2 123456789011 mail2@yahoo.com 115 | account-abcaws3 accnt-abcaws3 123456789011 mail3@yahoo.com 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Getting started with aws-orgs 2 | ============================= 3 | 4 | A configuration management tool set for AWS Organizations. 5 | 6 | Full documentation is available at https://aws-orgs.readthedocs.io/en/latest 7 | 8 | 9 | Features 10 | -------- 11 | 12 | - Ensure state of AWS Organizations and IAM resourses per `yaml`_ formatted 13 | specification files. 14 | - Configure AWS Organizations resources: 15 | 16 | - organizational units 17 | - service control policies 18 | - account creation and organizational unit placement 19 | 20 | - Centrally manage IAM access across AWS Organization accounts: 21 | 22 | - IAM users/groups in a central *Auth* account 23 | - customer managed IAM policies 24 | - IAM roles and trust delegation in organization accounts 25 | 26 | 27 | 28 | 29 | Installation 30 | ------------ 31 | 32 | Python virtual environment (recommended):: 33 | 34 | source ~/path_to_my_venv/bin/activate 35 | pip install aws-orgs 36 | 37 | 38 | Editable copy in venv:: 39 | 40 | git clone https://github.com/ucopacme/aws-orgs 41 | pip install -e aws-orgs/ 42 | 43 | 44 | Uninstall:: 45 | 46 | pip uninstall aws-orgs 47 | 48 | 49 | Configuration quick start 50 | ------------------------- 51 | 52 | Run the ``awsorgs-spec-init`` script to generate an initial set of spec-files:: 53 | 54 | awsorgs-spec-init 55 | 56 | This generates an initial ``config.yaml`` spec files under ``~/.awsorgs``. Edit 57 | these as needed to suit your environment. 58 | 59 | See ``--help`` option for full usage. 60 | 61 | 62 | 63 | Console Scripts 64 | --------------- 65 | 66 | ``aws-orgs`` provides the following python executibles: 67 | 68 | awsorgs 69 | Manage recources in an AWS Organization. 70 | 71 | awsaccounts 72 | Manage accounts in an AWS Organization. 73 | 74 | awsauth 75 | Manage users, group, and roles for cross account access in an 76 | AWS Organization. 77 | 78 | awsloginprofile 79 | Manage AWS IAM user login profile. 80 | 81 | 82 | All commands execute in ``dry-run`` mode by default. Include the ``--exec`` 83 | flag to affect change to AWS resources. Run each of these with the '--help' 84 | option for usage documentation. 85 | 86 | :: 87 | 88 | awsorgs report 89 | awsorgs organization 90 | awsorgs organization --exec 91 | 92 | awsaccounts report 93 | awsaccounts create [--exec] 94 | awsaccounts alias [--exec] 95 | 96 | awsaccounts invite --account-id ID [--exec] 97 | # from invited account: 98 | awsorgs-accessrole --master_id ID [--exec] 99 | 100 | awsauth report 101 | awsauth report --users 102 | awsauth report --delegations 103 | awsauth report --credentials --full 104 | awsauth report --account ucpath-prod --users --full 105 | 106 | awsauth users [--exec] 107 | awsauth delegations [--exec] 108 | awsauth local-users [--exec] 109 | 110 | awsloginprofile maryanne 111 | awsloginprofile maryanne --new 112 | awsloginprofile maryanne --reset 113 | awsloginprofile maryanne --disable-expired --opt-ttl 48 114 | 115 | 116 | 117 | :Author: 118 | Ashley Gould (agould@ucop.edu) 119 | 120 | :Version: 0.3.1 121 | 122 | 123 | 124 | 125 | .. references 126 | 127 | .. _yaml: https://yaml.org/ 128 | -------------------------------------------------------------------------------- /awsorgs/spec_init_data/spec.d/delegations.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # AWS Auth Delegations Specification 3 | # 4 | # A delegation is a complex of IAM resources which combine to allow 5 | # users in the ('trusted') Auth account to access and manipulate 6 | # resources in one or several or the other ('trusting') accounts in 7 | # the Organization. This is accomplished by managing a delegation 8 | # role in the trusting accounts which contains a trust policy naming 9 | # the Auth account as 'principal', and by assigning 'assume role' 10 | # policies to a managed group in the Auth account for each trusting 11 | # account within the scope of the delegation specification. 12 | # 13 | # Each delegation spec has the following attributes: 14 | # RoleName (str): The name of the IAM role created in trusting accounts. 15 | # Ensure ('present'[default]|'absent'): 16 | # Determines whether the IAM role exists or not. 17 | # Setting to 'absent' deletes delegation roles in 18 | # trusting accounts and removes assume role policies 19 | # from the trusted group. 20 | # Description (str): A decription applied to the IAM role. 21 | # Path (str): Path prefix for the IAM user resource name. (optional) 22 | # If Path is not fully qualified (i.e. starts with '/'), 23 | # awsauth prepends the 'default_path' to Path. 24 | # TrustingAccount (list(str), 'ALL'): 25 | # List of trusting accounts within the scope of the 26 | # delegation. If set to 'ALL', all accounts in the 27 | # Organization are include in the delegation. 28 | # ExcludeAccounts (list(str)): 29 | # If 'TrustingAccount' attribute is set to 'ALL', 30 | # any accounts listed in 'ExludeAccounts' are 31 | # excluded from the delegation. 32 | # TrustedGroup (str): The IAM group in the Auth account in which to assign 33 | # assume role policies for this delegation. 34 | # TrustedAccount (str): 35 | # The account Id to use as principle in service roles. 36 | # RequireMFA (bool): When set to 'True' (the default), add 37 | # a condition to the trust policy requiring users 38 | # assuming the delegation role to have valid MFA token. 39 | # Duration (int): MaxSessionDuration time in seconds. Default is 3600. 40 | # Policies (list(str)): 41 | # List of IAM policies to attach to the delegation role 42 | # in the trusting accounts. 43 | # PolicySet (str): Name of the policy set to attach to the delegation role 44 | # Incomplatible with "Policies". 45 | 46 | delegations: 47 | 48 | - RoleName: AccountAdmin 49 | Ensure: present 50 | Description: Full access to all services 51 | TrustingAccount: ALL 52 | ExcludeAccounts: 53 | - master-account 54 | TrustedGroup: admins 55 | RequireMFA: True 56 | Policies: 57 | - AdministratorAccess 58 | 59 | - RoleName: Developer 60 | Ensure: present 61 | Description: Allow developers access in dev1 account 62 | TrustingAccount: 63 | - dev1 64 | TrustedGroup: developers 65 | RequireMFA: True 66 | PolicySet: Developer 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /PUBLISHING.rst: -------------------------------------------------------------------------------- 1 | Steps for Publishing this Package to PyPI 2 | ========================================= 3 | 4 | Follow these steps to build and upload the package to PyPI. For more info, 5 | visit https://packaging.python.org/tutorials/packaging-projects 6 | 7 | 8 | Prerequisites 9 | ------------- 10 | 11 | - You must have access rights to post to both ``test.pypi.org`` and ``pypi.org`` 12 | - You must have `maintainer` status for this project on both ``test.pypi.org`` and ``pypi.org`` 13 | - Your python environment must have the latest updates to the required tools:: 14 | 15 | > pip install -U pip setuptools wheel twine 16 | 17 | 18 | Build the new release 19 | --------------------- 20 | 21 | After merging a pull request on Github: 22 | 23 | 1. Pull the new commits into your local master branch:: 24 | 25 | > git checkout master 26 | > git pull ucopacme master 27 | 28 | #. Edit ``awsorgs/__init__.py`` and update the ``__version__`` parameter to the new tag:: 29 | 30 | > git diff 31 | 32 | -__version__ = '0.3.0.dev0' 33 | +__version__ = '0.3.0' 34 | 35 | #. Build a distributable package with the ``setup.py`` script:: 36 | 37 | > python setup.py sdist bdist_wheel 38 | > ls -1 dist/ 39 | aws-orgs-0.3.0-py3-none-any.whl 40 | aws-orgs-0.3.0.tar.gz 41 | 42 | 43 | Validate distribution 44 | --------------------- 45 | 46 | #. Upload the new dist to the test PyPI site:: 47 | 48 | > twine upload --repository-url https://test.pypi.org/legacy/ dist/* 49 | Enter your username: agould 50 | Enter your password: ************ 51 | Uploading distributions to https://test.pypi.org/legacy/ 52 | Uploading aws-orgs-0.3.0.dev1-py3-none-any.whl 53 | Uploading aws-orgs-0.3.0-py3-none-any.whl 54 | 55 | #. Visit ``test.pypi.org`` and verify your release: https://test.pypi.org/project/aws-orgs/ 56 | 57 | #. Install the package into a clean python virtual environment:: 58 | 59 | > python -m venv package-test 60 | > source package-test/bin/activate 61 | > pip install --index-url https://test.pypi.org/simple/ --no-deps aws-orgs 62 | Looking in indexes: https://test.pypi.org/simple/ 63 | Collecting aws-orgs 64 | Successfully installed aws-orgs-0.3.0 65 | 66 | #. Verify the install:: 67 | 68 | > pip show aws-orgs 69 | Name: aws-orgs 70 | Version: 0.3.0 71 | Summary: Tools for working with AWS Organizations 72 | Home-page: https://github.com/ucopacme/aws-orgs 73 | 74 | 75 | Publish to public PyPI 76 | ---------------------- 77 | 78 | 1. Upload to real PyPI site:: 79 | 80 | > twine upload dist/* 81 | Enter your password: 82 | Uploading distributions to https://upload.pypi.org/legacy/ 83 | Uploading aws-orgs-0.3.0-py3-none-any.whl 84 | Uploading aws-orgs-0.3.0.tar.gz 85 | 86 | #. Visit ``pypi.org`` and verify your release: https://pypi.org/project/aws-orgs/ 87 | 88 | 89 | Tag and push to Github 90 | ---------------------- 91 | 92 | #. Be sure to commit the version update any other changes you made during package validation:: 93 | 94 | > git commit -am 'release 0.3.0' 95 | [master 04c3946] release 0.3.0 96 | 97 | #. Create a git tag for the new release:: 98 | 99 | > git tag -a -m 'Release 0.3.0' 0.3.0 100 | 101 | #. Push to master on github along with the new tag:: 102 | 103 | > git push ucopacme master --tags 104 | To github.com:ucopacme/aws-orgs.git 105 | 0035e5f..04c3946 master -> master 106 | * [new tag] 0.3.0 -> 0.3.0 107 | 108 | 109 | -------------------------------------------------------------------------------- /docs/source/usage/awsaccounts.rst: -------------------------------------------------------------------------------- 1 | Provissioning Accounts - ``awsaccounts`` 2 | ======================================== 3 | 4 | Prerequisites: 5 | 6 | - admin access to auth account 7 | - spec file setup 8 | 9 | - ``~/.awsorgs/config.yaml`` for awsorgs configuration parameters 10 | - spec files in ``spec_dir`` directory which is defined in ``config.yaml`` 11 | - create at least one satelite account (see ``awsaccounts``) 12 | 13 | 14 | 15 | Commands used: 16 | 17 | - awsaccounts report 18 | - awsaccounts create 19 | - awsaccounts create --exec 20 | - awsaccounts update 21 | - awsaccounts update --exec 22 | 23 | 24 | Spec files impacted: 25 | 26 | - ~/.awsorgs/config.yaml 27 | - spec_dir/accounts.yaml 28 | 29 | 30 | Actions Summary: 31 | 32 | - `Report accounts in an AWS Organization`_ 33 | - `Create a new AWS account`_ 34 | - `Set or update AWS account alias`_ 35 | - `Set or update AWS account tags`_ 36 | 37 | 38 | 39 | Report accounts in an AWS Organization 40 | ************************************** 41 | 42 | Run ``awsaccount report``:: 43 | 44 | (py36) [jhsu@scrappy-aws doc]$ awsaccounts report 45 | 46 | _______________________ 47 | Active Accounts in Org: 48 | 49 | Name: Alias Id: Email: 50 | account-abcaws1 acct-abcaws1 123456789011 mail1@yahoo.com 51 | account-abcaws2 acct-abcaws2 123456789011 mail2@yahoo.com 52 | account-abcaws3 accnt-abcaws3 123456789011 mail3@yahoo.com 53 | 54 | 55 | 56 | Create a new AWS account 57 | ************************ 58 | 59 | Edit file ``accounts.yaml`` 60 | 61 | Example Diff:: 62 | 63 | ~/.awsorgs/spec.d> git diff account-tags 64 | diff --git a/accounts.yaml b/accounts.yaml 65 | index 701d502..5224dc5 100644 66 | --- a/accounts.yaml 67 | +++ b/accounts.yaml 68 | @@ -72,3 +72,7 @@ accounts: 69 | +- Name: test3 70 | + Email: test3@example.com 71 | + Alias: 72 | + Tags: 73 | 74 | Review proposed changes in ``dry-run`` mode:: 75 | 76 | $ awsaccounts create 77 | 78 | Implement and review changes **!!WARNING!! D0 NOT RUN WITH --exec IF DOING 79 | FUNCTIONAL TESTING. It is a pain to remove unwanted acounts.** :: 80 | 81 | $ awsaccounts create --exec 82 | $ awsaccounts report 83 | 84 | 85 | 86 | Set or update AWS account alias 87 | ******************************* 88 | 89 | Edit file ``accounts.yaml`` 90 | 91 | Example Diff:: 92 | 93 | ~/.awsorgs/spec.d> git diff 94 | diff --git a/accounts.yaml b/accounts.yaml 95 | index 701d502..7f3bb83 100644 96 | --- a/accounts.yaml 97 | +++ b/accounts.yaml 98 | @@ -18,7 +18,7 @@ accounts: 99 | - Name: Managment 100 | - Alias: ashely-managment 101 | + Alias: central-auth 102 | Email: management@example.com 103 | Tags: 104 | 105 | 106 | Review proposed changes in ``dry-run`` mode:: 107 | 108 | $ awsaccounts update 109 | 110 | 111 | Implement and review changes:: 112 | 113 | $ awsaccounts update --exec 114 | $ awsaccounts report 115 | 116 | 117 | Set or update AWS account tags 118 | ****************************** 119 | 120 | Edit file ``accounts.yaml`` 121 | 122 | Example Diff:: 123 | 124 | ~/.awsorgs/spec.d> git diff 125 | diff --git a/accounts.yaml b/accounts.yaml 126 | index 7f3bb83..9f3f0d3 100644 127 | --- a/accounts.yaml 128 | +++ b/accounts.yaml 129 | @@ -21,7 +21,8 @@ accounts: 130 | Alias: central-auth 131 | Email: management@ucop.edu 132 | Tags: 133 | - owner: Ashley Gould 134 | + owner: Kumar Yegamani 135 | + service: infrastructure 136 | application: identity_mgmt 137 | environment: production 138 | 139 | Review proposed changes in ``dry-run`` mode:: 140 | 141 | $ awsaccounts update 142 | 143 | 144 | Implement:: 145 | 146 | 147 | $ awsaccounts update --exec 148 | 149 | Review changes - assume role into master account:: 150 | 151 | $ aws organizations list-tags-for-resource --resource-id 152 | 153 | -------------------------------------------------------------------------------- /docs/source/testing/test_awsloginprofile.rst: -------------------------------------------------------------------------------- 1 | Functional tests for awsorgs/awsloginprofile 2 | ============================================ 3 | 4 | Prerequisites: 5 | 6 | - admin access to auth account 7 | - spec file setup 8 | 9 | - ~/.awsorgs/config.yaml for awsorgs configuration parameters 10 | - spec files in spec_dir directory which is defined in config.yaml 11 | - create at least one satelite account (see awsaccounts) 12 | 13 | 14 | 15 | AWS awsloginprofile 16 | ------------------- 17 | 18 | Commands used: 19 | 20 | - awsauth users --exec 21 | - awsauth local-users --exec 22 | - awsloginprofile user --new 23 | - awsloginprofile user --reset 24 | - awsloginprofile user --report 25 | 26 | 27 | spec files impacted: 28 | 29 | - ~/.awsorgs/config.yaml 30 | - spec_dir/users-spec.yml 31 | - spec_dir/groups-spec.yml 32 | 33 | 34 | Actions Summary: 35 | 36 | - Define org_access_role in ~/.awsorgs/config.yaml 37 | - Define spec_dir/users-spec.yml 38 | - Define spec_dir/groups-spec.yml 39 | - awsauth users --exec 40 | - awsloginprofile user --new 41 | - awsloginprofile user --reset 42 | - awsloginprofile user --report 43 | - awsauth local-users --exec 44 | 45 | 46 | Define org_access_role in ~/.awsorgs/config.yaml 47 | ************************************************ 48 | 49 | Edit ~/.awsorgs/config.yaml :: 50 | 51 | org_access_role: awsauth/OrgAdmin 52 | 53 | 54 | 55 | Assume OrgAdmin role for creating new IAM user 56 | ********************************************** 57 | 58 | Assume auth acount administrtor role:: 59 | 60 | (py36) [jhsu@scrappy-aws awsorgs]$ aws-assume-role master-abc-OrgAdmin 61 | (py36) [jhsu@scrappy-aws awsorgs]$ 62 | (py36) [jhsu@scrappy-aws awsorgs]$ aws-whoami 63 | { 64 | "UserId": "AROAJS2JVTC6CC3YZX3BC:abc-admin@OrgAdmin", 65 | "Account": "123456789011", 66 | "Arn": "arn:aws:sts::123456789011:assumed-role/OrgAdmin/abc-admin@OrgAdmin" 67 | } 68 | 69 | 70 | 71 | Check current user login profile 72 | ******************************** 73 | 74 | Run 'awsloginprofile user' :: 75 | 76 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-1 77 | User: abcaws1-user-1 78 | Arn: arn:aws:iam::123456789011:user/awsauth/abcaws1-user-1 79 | User Id: AIDAI5DX7YNIPTLGTQXZK 80 | User created: 2019-01-03 01:28:59+00:00 81 | Login profile created: 2019-01-10 19:32:11+00:00 82 | Passwd reset required: True 83 | Password last used: 2019-01-10 19:55:35+00:00 84 | Delegations: 85 | Account Id Alias Role 86 | 2222222222 acct-abcaws1 awsauth/abcaws1 87 | 88 | 89 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-2 90 | no such user: abcaws1-user-2 91 | 92 | 93 | 94 | Create new IAM user in user-spec.yml and group-spec.yml 95 | ******************************************************* 96 | 97 | Edit users-spec.yml :: 98 | 99 | - Name: abcaws1-user-2 100 | Email: xyz@yahoo.com 101 | Team: team-abcaws1 102 | 103 | Edit groups-spec.yml :: 104 | 105 | - Name: group-abcaws1 106 | Members: 107 | - abcaws1-user-1 108 | - abcaws1-user-2 109 | 110 | 111 | 112 | Create new IAM user with awsauth 113 | ******************************** 114 | 115 | Run 'awsauth users --exec' :: 116 | 117 | (py36) [jhsu@scrappy-aws awsorgs]$ awsauth users --exec 118 | awsorgs.utils: INFO Creating user 'abcaws1-user-2' 119 | awsorgs.utils: INFO arn:aws:iam::123456789011:user/awsauth/abcaws1-user-2 120 | awsorgs.utils: INFO Adding user 'abcaws1-user-2' to group 'all-users' 121 | awsorgs.utils: INFO Adding user 'abcaws1-user-2' to group 'group-abcaws1' 122 | 123 | 124 | 125 | Create user login profile with awsloginprofile 126 | ********************************************** 127 | 128 | Run 'wsloginprofile user --new' :: 129 | 130 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-2 --new 131 | 132 | 133 | 134 | User will receive initial login instruction from email notification 135 | ******************************************************************* 136 | 137 | Check email titled "login profile" for initial AWS login instruction :: 138 | 139 | Dear User, 140 | 141 | You have been granted access to our central AWS authentication account. From here you can assume designated roles into other AWS accounts in our Organization. 142 | 143 | You must complete the following tasks to configure your access: 144 | 145 | 1) Use the credentials below to log into the AWS console. You will be required to change your password as you log in. The rules for good passwords are as follows: 146 | 147 | - Minimum password length: 8 148 | - Require at least one uppercase character from Latin alphabet. (A-Z) 149 | - Require at least one lowercase character from Latin alphabet. (a-z) 150 | - Require at least one symbol. (!@#$%^&*()_+-=[]{}|') 151 | - Require at least one number. (0-9) 152 | 153 | IMPORTANT: your one time password will expire after 24 hours. 154 | 155 | IAM User Name: abcaws1-user-2 156 | One Time Password: Stroller_Ochre+402_Disputed 157 | Login URL: https://master-aaa.signin.aws.amazon.com/console 158 | 159 | 160 | 161 | Check user login status 162 | *********************** 163 | 164 | Run 'wsloginprofile user' :: 165 | 166 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-2 167 | 168 | User: abcaws1-user-2 169 | Arn: arn:aws:iam::123456789011:user/awsauth/abcaws1-user-2 170 | User Id: AIDAJKHIBNEWTQ3T2QOYC 171 | User created: 2019-01-15 00:06:45+00:00 172 | Login profile created: 2019-01-15 00:07:08+00:00 173 | Passwd reset required: False 174 | Password last used: 2019-01-15 00:51:46+00:00 175 | Delegations: 176 | Account Id Alias Role 177 | 222222222222 acct-abcaws1 awsauth/abcaws1 178 | 179 | 180 | Reset user login profile(password) 181 | ********************************** 182 | 183 | Run 'wsloginprofile user --reset' :: 184 | 185 | awsloginprofile abcaws1-user-2 --reset 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /docs/source/usage/awsloginprofile.rst: -------------------------------------------------------------------------------- 1 | IAM User Login Profiles - ``awsloginprofile`` 2 | ============================================= 3 | 4 | Prerequisites: 5 | 6 | - admin access to auth account 7 | - spec file setup 8 | 9 | - ~/.awsorgs/config.yaml for awsorgs configuration parameters 10 | - spec files in spec_dir directory which is defined in config.yaml 11 | - create at least one satelite account (see awsaccounts) 12 | 13 | 14 | Commands used: 15 | 16 | - awsauth users --exec 17 | - awsloginprofile user --new 18 | - awsloginprofile user --reset 19 | - awsloginprofile user --report 20 | 21 | 22 | spec files impacted: 23 | 24 | - ~/.awsorgs/config.yaml 25 | - spec_dir/users.yaml 26 | - spec_dir/groups.yaml 27 | 28 | 29 | Actions Summary: 30 | 31 | - `Define org_access_role in ~/.awsorgs/config.yaml`_ 32 | - `Assume OrgAdmin role for creating new IAM user`_ 33 | - `Check current user login profile`_ 34 | - `Create new IAM user in users.yaml and groups.yaml`_ 35 | - `Create new IAM user with awsauth`_ 36 | - `Create user login profile with awsloginprofile`_ 37 | - `User will receive initial login instruction from email notification`_ 38 | - `Check user login status`_ 39 | - `Reset user login profile(password)`_ 40 | 41 | 42 | 43 | Define org_access_role in ``~/.awsorgs/config.yaml`` 44 | **************************************************** 45 | 46 | Edit ~/.awsorgs/config.yaml :: 47 | 48 | org_access_role: awsauth/OrgAdmin 49 | 50 | 51 | 52 | Assume ``OrgAdmin`` role for creating new IAM user 53 | ************************************************** 54 | 55 | Assume auth acount administrtor role:: 56 | 57 | (py36) [jhsu@scrappy-aws awsorgs]$ aws-assume-role master-abc-OrgAdmin 58 | (py36) [jhsu@scrappy-aws awsorgs]$ 59 | (py36) [jhsu@scrappy-aws awsorgs]$ aws-whoami 60 | { 61 | "UserId": "AROAJS2JVTC6CC3YZX3BC:abc-admin@OrgAdmin", 62 | "Account": "123456789011", 63 | "Arn": "arn:aws:sts::123456789011:assumed-role/OrgAdmin/abc-admin@OrgAdmin" 64 | } 65 | 66 | 67 | 68 | Check current user login profile 69 | ******************************** 70 | 71 | Run 'awsloginprofile user' :: 72 | 73 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-1 74 | User: abcaws1-user-1 75 | Arn: arn:aws:iam::123456789011:user/awsauth/abcaws1-user-1 76 | User Id: AIDAI5DX7YNIPTLGTQXZK 77 | User created: 2019-01-03 01:28:59+00:00 78 | Login profile created: 2019-01-10 19:32:11+00:00 79 | Passwd reset required: True 80 | Password last used: 2019-01-10 19:55:35+00:00 81 | Delegations: 82 | Account Id Alias Role 83 | 2222222222 acct-abcaws1 awsauth/abcaws1 84 | 85 | 86 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-2 87 | no such user: abcaws1-user-2 88 | 89 | 90 | 91 | Create new IAM user in ``users.yaml`` and ``groups.yaml`` 92 | ********************************************************* 93 | 94 | Edit users.yaml :: 95 | 96 | - Name: abcaws1-user-2 97 | Email: xyz@yahoo.com 98 | Team: team-abcaws1 99 | 100 | Edit groups.yaml :: 101 | 102 | - Name: group-abcaws1 103 | Members: 104 | - abcaws1-user-1 105 | - abcaws1-user-2 106 | 107 | 108 | 109 | Create new IAM user with ``awsauth`` 110 | ************************************ 111 | 112 | Run 'awsauth users --exec' :: 113 | 114 | (py36) [jhsu@scrappy-aws awsorgs]$ awsauth users --exec 115 | awsorgs.utils: INFO Creating user 'abcaws1-user-2' 116 | awsorgs.utils: INFO arn:aws:iam::123456789011:user/awsauth/abcaws1-user-2 117 | awsorgs.utils: INFO Adding user 'abcaws1-user-2' to group 'all-users' 118 | awsorgs.utils: INFO Adding user 'abcaws1-user-2' to group 'group-abcaws1' 119 | 120 | 121 | 122 | Create user login profile with ``awsloginprofile`` 123 | ************************************************** 124 | 125 | Run 'wsloginprofile user --new' :: 126 | 127 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-2 --new 128 | 129 | 130 | 131 | User will receive initial login instruction from email notification 132 | ******************************************************************* 133 | 134 | Check email titled "login profile" for initial AWS login instruction :: 135 | 136 | Dear User, 137 | 138 | You have been granted access to our central AWS authentication account. From here you can assume designated roles into other AWS accounts in our Organization. 139 | 140 | You must complete the following tasks to configure your access: 141 | 142 | 1) Use the credentials below to log into the AWS console. You will be required to change your password as you log in. The rules for good passwords are as follows: 143 | 144 | - Minimum password length: 8 145 | - Require at least one uppercase character from Latin alphabet. (A-Z) 146 | - Require at least one lowercase character from Latin alphabet. (a-z) 147 | - Require at least one symbol. (!@#$%^&*()_+-=[]{}|') 148 | - Require at least one number. (0-9) 149 | 150 | IMPORTANT: your one time password will expire after 24 hours. 151 | 152 | IAM User Name: abcaws1-user-2 153 | One Time Password: Stroller_Ochre+402_Disputed 154 | Login URL: https://master-aaa.signin.aws.amazon.com/console 155 | 156 | 157 | 158 | Check user login status 159 | *********************** 160 | 161 | Run 'wsloginprofile user' :: 162 | 163 | (py36) [jhsu@scrappy-aws awsorgs]$ awsloginprofile abcaws1-user-2 164 | 165 | User: abcaws1-user-2 166 | Arn: arn:aws:iam::123456789011:user/awsauth/abcaws1-user-2 167 | User Id: AIDAJKHIBNEWTQ3T2QOYC 168 | User created: 2019-01-15 00:06:45+00:00 169 | Login profile created: 2019-01-15 00:07:08+00:00 170 | Passwd reset required: False 171 | Password last used: 2019-01-15 00:51:46+00:00 172 | Delegations: 173 | Account Id Alias Role 174 | 222222222222 acct-abcaws1 awsauth/abcaws1 175 | 176 | 177 | Reset user login profile(password) 178 | ********************************** 179 | 180 | Run 'wsloginprofile user --reset' :: 181 | 182 | awsloginprofile abcaws1-user-2 --reset 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /docs/project_notes/TODO.txt: -------------------------------------------------------------------------------- 1 | 2 | __________________ 3 | TODO project: 4 | 5 | DONE finish README file 6 | DONE document spec-file structure 7 | DONE create setup.py and package project 8 | DONE pull functions out of __init__ into new module 9 | 10 | CONSIDER: 11 | a single interface executible with operation modes for all tasks: 12 | organization, accounts, users, delegation 13 | a single spec file awsorgs.conf which includes all the verious spec files 14 | allow per-account or per-team spec files for auth 15 | create a class to store args, logger, specs and 'deployed' dict for 16 | passing to functions. see branch 'named_tuple' 17 | 18 | 19 | 20 | 21 | 22 | 23 | __________________ 24 | utils.py 25 | 26 | TODO: 27 | NA in munge_path() check if default_path is defined or not 28 | DONE validate_spec() warn if spec contains non-defined attributes? 29 | 30 | CONSIDER: 31 | NOT should validate_spec() return a possibly altered specl? (e.g. value = 'default') 32 | 33 | 34 | 35 | __________________ 36 | TODO awsorgs.py: 37 | 38 | add unit testing 39 | IN PROGRESS change 'policy' to sc_policy everywhere 40 | 41 | DONE (hard to test) scan_deployed_accounts: fix 'NextToken' logic. 42 | DONE make master_id check a function. import into accounts. 43 | DONE control order of organization tasks 44 | DONE get rid of globals 45 | DONE make spec-file input and report output similar 46 | DONE create documentation (pydoc) 47 | DONE validate/sanitize org_spec input 48 | DONE get rid of var change_counter 49 | DONE send messages to a text accumutator or ?? 50 | DONE in aws-orgs.manage_accounts: 51 | DONE test account creation status running move_account() 52 | DONE in specify_policy_content: test keys exist 53 | DONE in manage_policy_attachments raise error when: 54 | DONE detaching default policy 55 | DONE attaching to an absent ou 56 | DONE in manage_policies: dont delete a policy attached to an ou 57 | 58 | NA in logger: prepend timestamp to messages 59 | NA make logger write to different url 60 | 61 | 62 | 63 | __________________ 64 | TODO accounts.py: 65 | 66 | set account email if not specified 67 | DONE apply new spec validation framework 68 | DONE derive Email attribute from domain_name 69 | DONE enforce use of Team attribute on managed accounts 70 | DONE import more functions from awsorgs 71 | DONE account creation 72 | DONE fill out validate_account_spec_file() 73 | DONE in scan_deployed_accounts: 74 | DONE crosscheck fully created accounts against States=['SUCCEEDED'])['CreateAccountStatuses'] 75 | 76 | CONSIDER: 77 | NOT parse account names for compliance 78 | NOT account-spec details allowed values for name components 79 | 80 | 81 | 82 | 83 | 84 | __________________ 85 | auth.py: 86 | 87 | TODO: 88 | 89 | delete unused custom policies. (non-attached to any role) 90 | see ListAccountsInOrganization in OrgMaster 91 | recreate role,policy if path changes 92 | report unmanaged iam resources in all accounts 93 | when reporting roles, handle service roles as well as 'IAM' roles 94 | harvest unmanaged roles in accounts which have the default path 95 | 96 | 97 | DONE incorporate create/send credentials for new users from awsorgs.loginprofile 98 | 99 | DONE require Team attribute for users 100 | DONE incorporate theading 101 | DONE in delegation report list accounts in alphabetical order 102 | DONE add 'Exclude' attribute to groups and delegations for when 'ALL' is specified 103 | DONE document spec structures 104 | DONE validate policy specs 105 | DONE create_groups: use boto group resourse 106 | DONE create_groups: after deleting a group, remove it from deployed['groups'] 107 | DONE validate delegation specs 108 | DONE handle 'ALL' special value in users[members] 109 | DONE reconsider spec param auth_account 110 | DONE handle 'ALL' special value in delegations[trusting_accounts] 111 | DONE warn when a user does not exist when populating groups 112 | DONE prevent deletion of org_access_role 113 | DONE document all functions 114 | DONE delete orphan delegations in accounts and groups 115 | DONE insert account name when reporting assume role policies 116 | DONE add debug option 117 | DONE handle assigning group policies in auth account. 118 | DONE check for custom policy updates 119 | DONE get org root id 120 | DONE report users, groups, roles 121 | DONE report roles and policies 122 | DONE create users 123 | DONE create groups 124 | DONE populate users in groups 125 | DONE create custom policies 126 | DONE attach policies to groups 127 | DONE create roles 128 | DONE populate roles in other org accounts. 129 | DONE replace functions get_{client/resource}_for_assumed_role() 130 | 131 | ISSUES: 132 | how/where do we structure/deploy yaml data for teams specification? 133 | maybe place it in a db or ldap or redis? 134 | currently this resides in the spec-file used by accounts.py. 135 | 136 | CONSIDER: 137 | should group get deleted even if it still has users? 138 | add options for reporting: 139 | allow separate reports for users, groups, delegations 140 | allow for multiple levels of detail in delegation reports 141 | add spec param 'use_team_path' as boolean: 142 | NOT append Path after team in munge_path() 143 | 144 | 145 | 146 | 147 | _______________ 148 | awsloginprofile 149 | 150 | TODO: 151 | 152 | prep_email(): don't just print() the email. where should it write to? 153 | prep_email(): add aws-shelltools usage, infohelp to upload ssh pubkey 154 | prep_email(): make html version? 155 | disable/reenable ssh keys 156 | email credentials new users 157 | requires an ses resource? 158 | require config file for email service? 159 | 160 | 161 | notes: 162 | 163 | subject = "Hello" 164 | html = "Hello Consumer" 165 | 166 | client = boto3.client('ses', region_name='us-east-1', aws_access_key_id="your_key", 167 | aws_secret_access_key="your_secret") 168 | 169 | client.send_email( 170 | Source='ACME ', 171 | Destination={'ToAddresses': [email]}, 172 | Message={ 173 | 'Subject': {'Data': subject}, 174 | 'Body': { 175 | 'Html': {'Data': html} 176 | } 177 | } 178 | ) 179 | 180 | other mailers: 181 | yagmail 182 | marrow.mailer 183 | 184 | 185 | http://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-an-email-using-sdk-programmatically.html 186 | 187 | 188 | 189 | 190 | ________________________________________________________________________________ 191 | ________________________________________________________________________________ 192 | 193 | 194 | -------------------------------------------------------------------------------- /awsorgs/spec.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import yaml 4 | 5 | import boto3 6 | from botocore.exceptions import ClientError 7 | from cerberus import Validator, schema_registry 8 | from pkg_resources import parse_version 9 | 10 | import awsorgs 11 | from awsorgs.utils import * 12 | from awsorgs.validator import file_validator, spec_validator 13 | 14 | # Spec parser defaults 15 | DEFAULT_CONFIG_FILE = '~/.awsorgs/config.yaml' 16 | DEFAULT_SPEC_DIR = '~/.awsorgs/spec.d' 17 | 18 | 19 | 20 | def scan_config_file(log, args): 21 | if args['--config']: 22 | config_file = args['--config'] 23 | else: 24 | config_file = DEFAULT_CONFIG_FILE 25 | config_file = os.path.expanduser(config_file) 26 | if not os.path.isfile(config_file): 27 | log.error("config_file not found: {}".format(config_file)) 28 | return None 29 | log.debug("loading config file: {}".format(config_file)) 30 | with open(config_file) as f: 31 | try: 32 | config = yaml.safe_load(f.read()) 33 | except (yaml.scanner.ScannerError, UnicodeDecodeError): 34 | log.error("{} not a valid yaml file".format(config_file)) 35 | return None 36 | except Exception as e: 37 | log.error("cant load config_file '{}': {}".format(config_file, e)) 38 | return None 39 | log.debug("config: {}".format(config)) 40 | return config 41 | 42 | 43 | def get_master_account_id(log, args, config): 44 | """ 45 | Determine the Org Master account id. Try in order: 46 | cli option, config file, client.describe_organization() 47 | """ 48 | if args['--master-account-id']: 49 | master_account_id = args['--master-account-id'] 50 | else: 51 | master_account_id = config.get('master_account_id') 52 | if master_account_id: 53 | if not valid_account_id(log, master_account_id): 54 | log.critical("config option 'master_account_id' is not valid account Id") 55 | sys.exit(1) 56 | else: 57 | log.debug("'master_account_id' not set in config_file or as cli option") 58 | try: 59 | master_account_id = boto3.client('organizations' 60 | ).describe_organization()['Organization']['MasterAccountId'] 61 | except ClientError as e: 62 | log.critical("can not determine master_account_id: {}".format(e)) 63 | sys.exit(1) 64 | log.debug("master_account_id: %s" % master_account_id) 65 | return master_account_id 66 | 67 | 68 | def get_spec_dir(log, args, config): 69 | """ 70 | Determine the spec directory. Try in order: 71 | cli option, config file, DEFAULT_SPEC_DIR. 72 | """ 73 | if '--spec-dir' in args and args['--spec-dir']: 74 | spec_dir = args['--spec-dir'] 75 | elif config['spec_dir']: 76 | spec_dir = config['spec_dir'] 77 | else: 78 | spec_dir = DEFAULT_SPEC_DIR 79 | spec_dir = os.path.expanduser(spec_dir) 80 | log.debug("spec_dir: %s" % spec_dir) 81 | return spec_dir 82 | 83 | 84 | def load_config(log, args): 85 | """ 86 | Assemble config options from various sources: cli options, config_file 87 | params, defaults, etc., and merge them into 'args' dict. 88 | When we are done we should have found all of the following: 89 | 90 | master_account_id 91 | org_access_role 92 | spec_dir (except when handling reports) 93 | auth_account_id (except when called by awsorgs) 94 | """ 95 | config = scan_config_file(log, args) 96 | args['--master-account-id'] = get_master_account_id(log, args, config) 97 | args['--spec-dir'] = get_spec_dir(log, args, config) 98 | if not args['--org-access-role']: 99 | args['--org-access-role'] = config.get('org_access_role') 100 | if not args['--auth-account-id']: 101 | args['--auth-account-id'] = config.get('auth_account_id') 102 | return args 103 | 104 | 105 | def validate_spec_file(log, spec_file, validator, errors): 106 | with open(spec_file) as f: 107 | try: 108 | spec_from_file = yaml.safe_load(f.read()) 109 | except (yaml.scanner.ScannerError, UnicodeDecodeError): 110 | log.warn("{} not a valid yaml file. skipping".format(spec_file)) 111 | return (None, errors) 112 | except Exception as e: 113 | log.error("cant load spec_file '{}': {}".format(spec_file, e)) 114 | return (None, errors) 115 | if validator.validate(spec_from_file): 116 | return (spec_from_file, errors) 117 | else: 118 | log.error("schema validation failed for spec_file: {}".format(spec_file)) 119 | log.debug("validator errors:\n{}".format(yamlfmt(validator.errors))) 120 | errors += 1 121 | return (None, errors) 122 | 123 | 124 | def validate_package_version(log, spec_dir): 125 | common_file_name = next( 126 | (file for file in os.listdir(spec_dir) if file.startswith('common')), 127 | None, 128 | ) 129 | if common_file_name is None: 130 | log.critical("cannot locate common spec file in spec_dir '{}'".format(spec_dir)) 131 | sys.exit(1) 132 | common_spec_file = os.path.join(spec_dir, common_file_name) 133 | log.debug('common spec file: {}'.format(common_spec_file)) 134 | with open(common_spec_file) as f: 135 | try: 136 | common_spec = yaml.safe_load(f.read()) 137 | except Exception as e: 138 | log.critical("cant load common spec file '{}': {}".format(common_spec_file, e)) 139 | sys.exit(1) 140 | log.debug('minimum_version: {}'.format(common_spec['minimum_version'])) 141 | if not parse_version(awsorgs.__version__) >= parse_version(common_spec['minimum_version']): 142 | log.critical('Installed aws-orgs package does not meet minimum version requirement. ' 143 | 'Please update your aws-orgs package to version "{}" or higher.'.format( 144 | common_spec['minimum_version'] 145 | )) 146 | sys.exit(1) 147 | return 148 | 149 | 150 | def validate_spec(log, args): 151 | """ 152 | Load all spec files in spec_dir and validate against spec schema 153 | """ 154 | 155 | # validate spec_files 156 | spec_dir = args['--spec-dir'] 157 | if not os.path.isdir(spec_dir): 158 | log.error("spec_dir not found or not a directory: {}".format(spec_dir)) 159 | sys.exit(1) 160 | validate_package_version(log, spec_dir) 161 | validator = file_validator(log) 162 | spec_object = {} 163 | errors = 0 164 | for dirpath, dirnames, filenames in os.walk(spec_dir, topdown = True): 165 | dirnames[:] = [d for d in dirnames if not d.startswith('.')] 166 | for f in filenames: 167 | log.debug("considering file {}".format(f)) 168 | spec_from_file, errors = validate_spec_file(log, 169 | os.path.join(dirpath, f), validator, errors) 170 | if spec_from_file: 171 | spec_object.update(spec_from_file) 172 | if errors: 173 | log.critical("schema validation failed for {} spec files. run in debug mode for details".format(errors)) 174 | sys.exit(1) 175 | log.debug("spec_object:\n{}".format(yamlfmt(spec_object))) 176 | 177 | # validate aggregated spec_object 178 | validator = spec_validator(log) 179 | if not validator.validate(spec_object): 180 | log.critical("spec_object validation failed:\n{}".format( 181 | yamlfmt(validator.errors))) 182 | sys.exit(1) 183 | log.debug("spec_object validation succeeded") 184 | return spec_object 185 | -------------------------------------------------------------------------------- /docs/source/usage/awsauth-users.rst: -------------------------------------------------------------------------------- 1 | Users and Groups - ``awsauth users`` 2 | ===================================== 3 | 4 | Prerequisites: 5 | 6 | - admin access to auth account 7 | - spec file setup 8 | 9 | - install template spec files 10 | - spec files under git 11 | - site specific paramaters defined in common.yaml 12 | - configure ~.awsorgs/config.yaml 13 | - create at least one satelite account (see awsaccounts) 14 | 15 | 16 | Commands used: 17 | 18 | - git diff 19 | - awsauth users 20 | - awsauth users --exec 21 | - awsauth report --users 22 | 23 | 24 | Spec files impacted: 25 | 26 | - users.yaml 27 | - groups.yaml 28 | - custom_policies.yaml 29 | 30 | 31 | Actions Summary: 32 | 33 | - `Report users and groups in all accounts`_ 34 | - `Create an IAM user and group, and add the user to the group`_ 35 | - `Attach a IAM managed policy to your group`_ 36 | - `Attach a IAM custom policy to your group`_ 37 | - `Modify attached custom policy`_ 38 | - `Detach policies, users from group`_ 39 | - `Delete group, delete users`_ 40 | 41 | 42 | 43 | Report users and groups in all accounts 44 | *************************************** 45 | 46 | Run ``awsauth report`` command with ``--users`` flag:: 47 | 48 | $ awsauth report --users 49 | _________________________________________ 50 | IAM Users and Groups in all Org Accounts: 51 | _____________________ 52 | Account: Managment 53 | Users: 54 | - arn:aws:iam::123456789011:user/awsauth/sysadm/agould 55 | - arn:aws:iam::123456789011:user/awsauth/drivera 56 | - arn:aws:iam::123456789011:user/awsauth/jhsu 57 | 58 | Groups: 59 | - arn:aws:iam::123456789011:group/awsauth/all-users 60 | - arn:aws:iam::123456789011:group/awsauth/orgadmins 61 | 62 | ____________________ 63 | Account: blee-dev 64 | ____________________ 65 | Account: blee-poc 66 | _____________________ 67 | Account: blee-prod 68 | __________________ 69 | Account: master 70 | Users: 71 | - arn:aws:iam::222222222222:user/agould 72 | 73 | Groups: 74 | - arn:aws:iam::222222222222:group/Admins 75 | 76 | 77 | Some variations:: 78 | 79 | $ awsauth report --users --full --account Managment 80 | $ awsauth report --users --full 81 | $ awsauth report --credentials --account Managment 82 | $ awsauth report --credentials 83 | 84 | 85 | 86 | Create an IAM user and group, and add the user to the group 87 | *********************************************************** 88 | 89 | Edit the following files: 90 | 91 | - users.yaml 92 | - groups.yaml 93 | 94 | Example Diff:: 95 | 96 | ~/.awsorgs/spec.d> git diff 97 | diff --git a/groups.yaml b/groups.yaml 98 | index 7f37144..d3fe879 100644 99 | --- a/groups.yaml 100 | +++ b/groups.yaml 101 | @@ -46,3 +46,8 @@ groups: 102 | 103 | + - Name: testers 104 | + Ensure: present 105 | + Members: 106 | + - joeuser 107 | + - maryuser 108 | diff --git a/users.yaml b/users.yaml 109 | index 22d2d61..5424bf4 100644 110 | --- a/users.yaml 111 | +++ b/users.yaml 112 | @@ -36,3 +36,6 @@ users: 113 | 114 | + - Name: joeuser 115 | + Email: joeuser@example.com 116 | + Team: test 117 | + - Name: maryuser 118 | + Email: maryuser@example.com 119 | + Team: test 120 | 121 | Review proposed changes in ``dry-run`` mode:: 122 | 123 | $ awsauth users 124 | 125 | Implement and review changes:: 126 | 127 | $ awsauth users --exec 128 | $ awsauth report --users 129 | 130 | 131 | Attach a IAM managed policy to your group 132 | ***************************************** 133 | 134 | Edit file ``groups.yaml`` 135 | 136 | Example Diff:: 137 | 138 | ~/.awsorgs/spec.d> git diff 139 | diff --git a/groups.yaml b/groups.yaml 140 | index d3fe879..9e05738 100644 141 | --- a/groups.yaml 142 | +++ b/groups.yaml 143 | @@ -50,4 +50,6 @@ groups: 144 | - Name: testers 145 | Ensure: present 146 | Members: 147 | - joeuser 148 | - maryuser 149 | + Policies: 150 | + - IAMReadOnlyAccess 151 | 152 | Review proposed changes in ``dry-run`` mode:: 153 | 154 | $ awsauth users 155 | 156 | Implement and review changes:: 157 | 158 | $ awsauth users --exec 159 | $ aws iam list-attached-group-policies --group-name testers 160 | 161 | 162 | Attach a IAM custom policy to your group 163 | **************************************** 164 | 165 | Edit the following files: 166 | 167 | - groups.yaml 168 | - custom_policies.yaml 169 | 170 | Example Diff:: 171 | 172 | ~/.awsorgs/spec.d> git diff 173 | diff --git a/custom_policies.yaml b/custom_policies.yaml 174 | index da46ebb..5d411f0 100644 175 | --- a/custom_policies.yaml 176 | +++ b/custom_policies.yaml 177 | @@ -111,3 +111,14 @@ custom_policies: 178 | Action: 179 | - aws-portal:Account* 180 | Resource: '*' 181 | + 182 | + - PolicyName: ReadS3Bucket 183 | + Description: list and get objects from my s3 bucket 184 | + Statement: 185 | + - Effect: Allow 186 | + Action: 187 | + - s3:List* 188 | + - s3:Get* 189 | + Resource: 190 | + - arn:aws:s3:::my_bucket 191 | + - arn:aws:s3:::my_bucket/* 192 | diff --git a/groups.yaml b/groups.yaml 193 | index b506856..11e87cb 100644 194 | --- a/groups.yaml 195 | +++ b/groups.yaml 196 | @@ -36,3 +36,4 @@ groups: 197 | - maryuser 198 | Policies: 199 | - IAMReadOnlyAccess 200 | + - ReadS3Bucket 201 | 202 | 203 | Review proposed changes in ``dry-run`` mode:: 204 | 205 | $ awsauth users 206 | 207 | Implement and review changes:: 208 | 209 | $ awsauth users --exec 210 | $ aws iam list-attached-group-policies --group-name testers 211 | $ aws iam get-policy --policy-arn 212 | 213 | 214 | .. _modify_custom_policy: 215 | 216 | Modify attached custom policy 217 | ***************************** 218 | 219 | Edit file ``custom_policies.yaml`` 220 | 221 | Example Diff:: 222 | 223 | ~/.awsorgs/spec.d> git diff 224 | diff --git a/custom_policies.yaml b/custom_policies.yaml 225 | index d6f29d7..7f5748a 100644 226 | --- a/custom_policies.yaml 227 | +++ b/custom_policies.yaml 228 | @@ -131,6 +131,8 @@ custom_policies: 229 | Resource: 230 | - arn:aws:s3:::my_bucket 231 | - arn:aws:s3:::my_bucket/* 232 | + - arn:aws:s3:::my_other_bucket 233 | + - arn:aws:s3:::my_other_bucket/* 234 | 235 | 236 | Review proposed changes in ``dry-run`` mode:: 237 | 238 | $ awsauth users 239 | 240 | Implement and review changes:: 241 | 242 | $ awsauth users --exec 243 | $ aws iam list-attached-group-policies --group-name testers 244 | $ aws iam get-policy --policy-arn 245 | $ aws iam get-policy-version --policy-arn --version-id 246 | 247 | 248 | Detach policies, users from group 249 | ********************************* 250 | 251 | Edit the following files: 252 | 253 | - groups.yaml 254 | 255 | Example Diff:: 256 | 257 | (python3.6) ashely@horus:~/.awsorgs/spec.d> git diff 258 | diff --git a/groups.yaml b/groups.yaml 259 | index 9e05738..565b1ab 100644 260 | --- a/groups.yaml 261 | +++ b/groups.yaml 262 | @@ -49,7 +49,4 @@ groups: 263 | - Name: testers 264 | Ensure: present 265 | Members: 266 | - - joeuser 267 | - - maryuser 268 | Policies: 269 | - - IAMReadOnlyAccess 270 | - - ReadS3Bucket 271 | 272 | 273 | Review proposed changes in ``dry-run`` mode:: 274 | 275 | $ awsauth users 276 | 277 | Implement and review changes:: 278 | 279 | $ awsauth users --exec 280 | $ awsauth report --users 281 | $ aws iam list-attached-group-policies --group-name testers 282 | $ aws iam get-policy --policy-arn 283 | 284 | 285 | Delete group, delete users 286 | ************************** 287 | 288 | Files to edit: 289 | 290 | - users.yaml 291 | - groups.yaml 292 | 293 | To delete IAM entities we must set attribute ``Ensure: absent`` to associated spec. 294 | 295 | Example diff:: 296 | 297 | (python3.6) ashely@horus:~/.awsorgs/spec.d> git diff 298 | diff --git a/groups.yaml b/groups.yaml 299 | index 9e05738..4eda72b 100644 300 | --- a/groups.yaml 301 | +++ b/groups.yaml 302 | @@ -47,9 +47,6 @@ groups: 303 | 304 | - Name: testers 305 | - Ensure: present 306 | + Ensure: absent 307 | Members: 308 | Policies: 309 | diff --git a/users.yaml b/users.yaml 310 | index 5424bf4..3e8b87d 100644 311 | --- a/users.yaml 312 | +++ b/users.yaml 313 | @@ -37,5 +37,6 @@ users: 314 | - Name: joeuser 315 | + Ensure: absent 316 | Email: joeuser@example.com 317 | Team: test 318 | - Name: maryuser 319 | + Ensure: absent 320 | Email: maryuser@example.com 321 | Team: test 322 | 323 | 324 | Review proposed changes in ``dry-run`` mode:: 325 | 326 | $ awsauth users 327 | 328 | Implement and review changes:: 329 | 330 | $ awsauth users --exec 331 | $ awsauth report --users 332 | -------------------------------------------------------------------------------- /awsorgs/validator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Spec validator schema data 3 | 4 | ISSUES: 5 | place regex rule on email addresses, domain name 6 | """ 7 | import yaml 8 | 9 | from cerberus import Validator, schema_registry 10 | from awsorgs.utils import yamlfmt 11 | 12 | 13 | # Schema for validating spec files. Since spec is accumulated from multiple 14 | # files, we do not place a 'require' rule on first level keys. Individual spec 15 | # files have only a subset of these. 16 | # 17 | SPEC_FILE_SCHEMA = """ 18 | minimum_version: 19 | type: string 20 | master_account_id: 21 | type: string 22 | auth_account_id: 23 | type: string 24 | default_domain: 25 | type: string 26 | default_sc_policy: 27 | type: string 28 | default_ou: 29 | type: string 30 | default_path: 31 | type: string 32 | default_smtp_server: 33 | type: string 34 | org_admin_email: 35 | type: string 36 | organizational_units: 37 | required: False 38 | type: list 39 | schema: 40 | type: dict 41 | schema: organizational_unit 42 | sc_policies: 43 | required: False 44 | type: list 45 | schema: 46 | type: dict 47 | schema: sc_policy 48 | accounts: 49 | required: False 50 | type: list 51 | schema: 52 | type: dict 53 | schema: account 54 | users: 55 | required: False 56 | type: list 57 | schema: 58 | type: dict 59 | schema: user 60 | groups: 61 | required: False 62 | type: list 63 | schema: 64 | type: dict 65 | schema: group 66 | delegations: 67 | required: False 68 | type: list 69 | schema: 70 | type: dict 71 | schema: delegation 72 | local_users: 73 | required: False 74 | type: list 75 | schema: 76 | type: dict 77 | schema: local_user 78 | custom_policies: 79 | required: False 80 | type: list 81 | schema: 82 | type: dict 83 | schema: custom_policy 84 | policy_sets: 85 | required: False 86 | type: list 87 | schema: 88 | type: dict 89 | schema: policy_set 90 | """ 91 | 92 | 93 | # Schema for validating the fully accumulate spec object. This is where we 94 | # ensure all required keys are present. But we do not need to check sub 95 | # schema, as that is done during spec_file validation. 96 | # 97 | SPEC_SCHEMA = """ 98 | minimum_version: 99 | required: True 100 | type: string 101 | master_account_id: 102 | required: True 103 | type: string 104 | auth_account_id: 105 | required: True 106 | type: string 107 | default_domain: 108 | required: True 109 | type: string 110 | default_sc_policy: 111 | required: True 112 | type: string 113 | default_ou: 114 | required: True 115 | type: string 116 | default_path: 117 | required: True 118 | type: string 119 | default_smtp_server: 120 | required: True 121 | type: string 122 | org_admin_email: 123 | required: True 124 | type: string 125 | organizational_units: 126 | required: True 127 | type: list 128 | sc_policies: 129 | required: True 130 | type: list 131 | accounts: 132 | required: True 133 | type: list 134 | users: 135 | required: True 136 | type: list 137 | groups: 138 | required: True 139 | type: list 140 | delegations: 141 | required: True 142 | type: list 143 | local_users: 144 | required: True 145 | type: list 146 | custom_policies: 147 | required: True 148 | type: list 149 | policy_sets: 150 | required: False 151 | nullable: True 152 | type: list 153 | """ 154 | 155 | 156 | ORGANIZATIONAL_UNIT_SCHEMA = """ 157 | Name: 158 | required: True 159 | type: string 160 | Accounts: 161 | required: False 162 | nullable: True 163 | type: list 164 | schema: 165 | type: string 166 | Child_OU: 167 | required: False 168 | nullable: True 169 | type: list 170 | schema: 171 | type: dict 172 | schema: organizational_unit 173 | SC_Policies: 174 | required: False 175 | nullable: True 176 | type: list 177 | schema: 178 | type: string 179 | Ensure: 180 | required: False 181 | type: string 182 | allowed: 183 | - present 184 | - absent 185 | """ 186 | 187 | POLICY_SCHEMA = """ 188 | PolicyName: 189 | required: True 190 | type: string 191 | Description: 192 | required: False 193 | type: string 194 | Statement: 195 | required: True 196 | anyof: 197 | - type: string 198 | - type: list 199 | schema: 200 | type: dict 201 | Ensure: 202 | required: False 203 | type: string 204 | allowed: 205 | - present 206 | - absent 207 | """ 208 | 209 | ACCOUNT_SCHEMA = """ 210 | Name: 211 | required: True 212 | type: string 213 | Email: 214 | required: False 215 | type: string 216 | Alias: 217 | required: False 218 | type: string 219 | Tags: 220 | required: False 221 | nullable: True 222 | type: dict 223 | allow_unknown: 224 | type: string 225 | """ 226 | 227 | USER_SCHEMA = """ 228 | Name: 229 | required: True 230 | type: string 231 | Email: 232 | required: True 233 | type: string 234 | CN: 235 | required: True 236 | type: string 237 | RequestId: 238 | required: True 239 | type: string 240 | Ensure: 241 | required: False 242 | type: string 243 | allowed: 244 | - present 245 | - absent 246 | """ 247 | 248 | GROUP_SCHEMA = """ 249 | Name: 250 | required: True 251 | type: string 252 | Path: 253 | required: False 254 | type: string 255 | nullable: True 256 | Members: 257 | required: False 258 | nullable: True 259 | anyof: 260 | - type: string 261 | allowed: 262 | - ALL 263 | - type: list 264 | schema: 265 | type: string 266 | ExcludeMembers: 267 | required: False 268 | nullable: True 269 | type: list 270 | schema: 271 | type: string 272 | Policies: 273 | required: False 274 | nullable: True 275 | type: list 276 | schema: 277 | type: string 278 | Ensure: 279 | required: False 280 | type: string 281 | allowed: 282 | - present 283 | - absent 284 | """ 285 | 286 | LOCAL_USER_SCHEMA = """ 287 | Name: 288 | required: True 289 | type: string 290 | ContactEmail: 291 | required: True 292 | type: string 293 | RequestId: 294 | required: True 295 | type: string 296 | Description: 297 | required: False 298 | type: string 299 | Service: 300 | required: True 301 | type: string 302 | Account: 303 | required: True 304 | anyof: 305 | - type: string 306 | allowed: 307 | - ALL 308 | - type: list 309 | schema: 310 | type: string 311 | ExcludeAccounts: 312 | required: False 313 | type: list 314 | schema: 315 | type: string 316 | Policies: 317 | required: False 318 | type: list 319 | schema: 320 | type: string 321 | Ensure: 322 | required: False 323 | type: string 324 | allowed: 325 | - present 326 | - absent 327 | """ 328 | 329 | DELEGATION_SCHEMA = """ 330 | RoleName: 331 | required: True 332 | type: string 333 | Description: 334 | required: False 335 | type: string 336 | TrustingAccount: 337 | required: True 338 | anyof: 339 | - type: string 340 | allowed: 341 | - ALL 342 | - type: list 343 | schema: 344 | type: string 345 | ExcludeAccounts: 346 | required: False 347 | type: list 348 | schema: 349 | type: string 350 | TrustedGroup: 351 | required: False 352 | type: string 353 | TrustedAccount: 354 | required: False 355 | type: string 356 | RequireMFA: 357 | required: False 358 | type: boolean 359 | Policies: 360 | required: True 361 | type: list 362 | schema: 363 | type: string 364 | excludes: PolicySet 365 | PolicySet: 366 | required: True 367 | type: string 368 | excludes: Policies 369 | Path: 370 | required: False 371 | type: string 372 | nullable: True 373 | Duration: 374 | required: False 375 | type: integer 376 | min: 3600 377 | max: 43200 378 | Ensure: 379 | required: False 380 | type: string 381 | allowed: 382 | - present 383 | - absent 384 | """ 385 | 386 | POLICY_SET_SCHEMA = """ 387 | Name: 388 | required: True 389 | type: string 390 | Description: 391 | required: False 392 | nullable: True 393 | type: string 394 | Policies: 395 | required: True 396 | nullable: True 397 | type: list 398 | schema: 399 | type: string 400 | Tags: 401 | required: False 402 | nullable: True 403 | type: list 404 | schema: 405 | type: dict 406 | schema: tag 407 | """ 408 | 409 | TAG_SCHEMA = """ 410 | Key: 411 | required: True 412 | type: string 413 | Value: 414 | required: False 415 | type: string 416 | """ 417 | 418 | 419 | 420 | def file_validator(log): 421 | schema_registry.add('organizational_unit', yaml.safe_load(ORGANIZATIONAL_UNIT_SCHEMA)) 422 | schema_registry.add('sc_policy', yaml.safe_load(POLICY_SCHEMA)) 423 | schema_registry.add('account', yaml.safe_load(ACCOUNT_SCHEMA)) 424 | schema_registry.add('user', yaml.safe_load(USER_SCHEMA)) 425 | schema_registry.add('group', yaml.safe_load(GROUP_SCHEMA)) 426 | schema_registry.add('local_user', yaml.safe_load(LOCAL_USER_SCHEMA)) 427 | schema_registry.add('delegation', yaml.safe_load(DELEGATION_SCHEMA)) 428 | schema_registry.add('custom_policy', yaml.safe_load(POLICY_SCHEMA)) 429 | schema_registry.add('policy_set', yaml.safe_load(POLICY_SET_SCHEMA)) 430 | schema_registry.add('tag', yaml.safe_load(TAG_SCHEMA)) 431 | log.debug("adding subschema to schema_registry: {}".format( 432 | schema_registry.all().keys())) 433 | vfile = Validator(yaml.safe_load(SPEC_FILE_SCHEMA)) 434 | log.debug("file_validator_schema: {}".format(vfile.schema)) 435 | return vfile 436 | 437 | 438 | def spec_validator(log): 439 | vspec = Validator(yaml.safe_load(SPEC_SCHEMA)) 440 | log.debug("spec_validator_schema: {}".format(vspec.schema)) 441 | return vspec 442 | -------------------------------------------------------------------------------- /docs/source/usage/awsauth-delegations.rst: -------------------------------------------------------------------------------- 1 | Cross Account Access Delegations - ``awsauth delegations`` 2 | ========================================================== 3 | 4 | Prerequisites: 5 | 6 | - IAM group with users to use as ``TrustedGroup`` 7 | 8 | 9 | Commands used: 10 | 11 | - git diff 12 | - awsauth delegations 13 | - awsauth delegations --exec 14 | - awsauth report --roles 15 | 16 | 17 | Spec files impacted: 18 | 19 | - delegations.yaml 20 | - custom_policies.yaml 21 | - policy_sets.yaml 22 | 23 | 24 | Actions: 25 | 26 | - `Create a cross account access delegation`_ 27 | - `Update the delegation to apply to all accounts`_ 28 | - `Update attributes of a delegation`_ 29 | - `Exclude some accounts from a delegation`_ 30 | - `Update the delegation attributes`_ 31 | - `Attach a custom policy`_ 32 | - `Modify a custom policy`_ 33 | - `Create a policy set and apply it to the delegation`_ 34 | - `Modify attributes of a policy set`_ 35 | - `Delete the delegation from all accounts`_ 36 | 37 | 38 | Create a cross account access delegation 39 | **************************************** 40 | 41 | File to edit: delegations.yaml 42 | 43 | - set ``TrustedGroup`` to your new group 44 | - define a list of accounts in ``TrustingAccount`` 45 | - define one managed policy in ``Policies`` 46 | 47 | Example Diff:: 48 | 49 | ~/.awsorgs/spec.d> git diff 50 | diff --git a/delegations.yaml b/delegations.yaml 51 | index 1ae3245..4d571e9 100644 52 | --- a/delegations.yaml 53 | +++ b/delegations.yaml 54 | @@ -101,3 +101,14 @@ delegations: 55 | 56 | + - RoleName: TestersRole 57 | + Ensure: present 58 | + Description: testing cross account delegation 59 | + TrustingAccount: 60 | + - dev1 61 | + TrustedGroup: testers 62 | + RequireMFA: True 63 | + Policies: 64 | + - ReadOnlyAccess 65 | 66 | 67 | Review proposed changes in ``dry-run`` mode:: 68 | 69 | $ awsauth delegations 70 | 71 | Implement and review changes:: 72 | 73 | $ awsauth delegations --exec 74 | $ awsauth report --roles | egrep "^Account|TestersRole" 75 | $ aws iam list-group-policies --group-name testers 76 | 77 | 78 | Update the delegation attributes 79 | ******************************** 80 | 81 | File to edit: delegations.yaml 82 | 83 | - add a ``Path`` attribute to the delegation 84 | - update ``Description`` attribute 85 | - update ``TrustingAccount`` attribute 86 | - update ``TrustedGroup`` attribute 87 | - update ``Policies`` attribute 88 | 89 | Example Diff:: 90 | 91 | 92 | 93 | Update the delegation to apply to all accounts 94 | ********************************************** 95 | 96 | File to edit: delegations.yaml 97 | 98 | - set ``TrustingAccount`` to keyword ``ALL`` 99 | 100 | Example Diff:: 101 | 102 | ~/.awsorgs/spec.d> git diff 103 | diff --git a/delegations.yaml b/delegations.yaml 104 | index 282db35..e46ac9e 100644 105 | --- a/delegations.yaml 106 | +++ b/delegations.yaml 107 | @@ -104,14 +104,10 @@ delegations: 108 | - RoleName: TestersRole 109 | Ensure: present 110 | Description: testing cross account delegation 111 | - TrustingAccount: 112 | - - dev1 113 | + TrustingAccount: ALL 114 | TrustedGroup: testers 115 | RequireMFA: True 116 | Policies: 117 | - ReadOnlyAccess 118 | 119 | Review proposed changes in ``dry-run`` mode:: 120 | 121 | $ awsauth delegations 122 | 123 | Implement and review changes:: 124 | 125 | $ awsauth delegations --exec 126 | $ awsauth report --roles | egrep "^Account|TestersRole" 127 | $ aws iam list-group-policies --group-name testers 128 | $ aws iam get-group-policy --group-name testers --policy-name AllowAssumeRole-TestersRole 129 | 130 | 131 | Update attributes of a delegation 132 | ********************************* 133 | 134 | File to edit: delegations.yaml 135 | 136 | - set a custom ``Path`` 137 | - alter the ``Description`` 138 | - add an additional policy 139 | 140 | Example Diff:: 141 | 142 | ~/.awsorgs/spec.d> git diff 143 | diff --git a/delegations.yaml b/delegations.yaml 144 | index 282db35..e46ac9e 100644 145 | --- a/delegations.yaml 146 | +++ b/delegations.yaml 147 | @@ -104,14 +104,10 @@ delegations: 148 | - RoleName: TestersRole 149 | Ensure: present 150 | - Description: testing cross account delegation 151 | + Description: testing cross account delegation role 152 | + Path: testing 153 | TrustingAccount: ALL 154 | TrustedGroup: testers 155 | RequireMFA: True 156 | Policies: 157 | - ReadOnlyAccess 158 | + - ViewBilling 159 | 160 | Review proposed changes in ``dry-run`` mode:: 161 | 162 | $ awsauth delegations 163 | 164 | Implement and review changes:: 165 | 166 | $ awsauth delegations --exec 167 | $ awsauth report --roles | egrep "^Account|TestersRole" 168 | $ aws iam get-role --role-name TestersRole | egrep "Path|Description" 169 | $ aws iam list-attached-role-policies --role-name TestersRole 170 | 171 | 172 | 173 | Exclude some accounts from a delegation 174 | *************************************** 175 | 176 | File to edit: delegations.yaml 177 | 178 | - define a list of accounts in ``ExcludeAccounts`` 179 | 180 | Example Diff:: 181 | 182 | :~/.awsorgs/spec.d> git diff 183 | diff --git a/delegations.yaml b/delegations.yaml 184 | index e46ac9e..8b01bb8 100644 185 | --- a/delegations.yaml 186 | +++ b/delegations.yaml 187 | @@ -105,6 +105,10 @@ delegations: 188 | Ensure: present 189 | Description: testing cross account delegation 190 | TrustingAccount: ALL 191 | + ExcludeAccounts: 192 | + - master 193 | TrustedGroup: testers 194 | RequireMFA: True 195 | 196 | 197 | Review proposed changes in ``dry-run`` mode:: 198 | 199 | $ awsauth delegations 200 | 201 | Implement and review changes:: 202 | 203 | $ awsauth delegations --exec 204 | $ awsauth report --roles | egrep "^Account|TestersRole" 205 | $ aws iam list-group-policies --group-name testers 206 | $ aws iam get-group-policy --group-name testers --policy-name AllowAssumeRole-TestersRole 207 | $ aws iam get-group-policy --group-name testers --policy-name DenyAssumeRole-TestersRole 208 | 209 | 210 | Attach a custom policy 211 | ********************** 212 | 213 | Files to edit: 214 | 215 | - custom_policies.yaml 216 | - delegations.yaml 217 | 218 | Example Diff:: 219 | 220 | ~/.awsorgs/spec.d> git diff 221 | diff --git a/custom_policies.yaml b/custom_policies.yaml 222 | index 9399a60..a428164 100644 223 | --- a/custom_policies.yaml 224 | +++ b/custom_policies.yaml 225 | @@ -120,3 +120,14 @@ custom_policies: 226 | + 227 | + - PolicyName: ReadS3Bucket 228 | + Description: list and get objects from my s3 bucket 229 | + Statement: 230 | + - Effect: Allow 231 | + Action: 232 | + - s3:List* 233 | + - s3:Get* 234 | + Resource: 235 | + - arn:aws:s3:::my_bucket 236 | + - arn:aws:s3:::my_bucket/* 237 | diff --git a/delegations.yaml b/delegations.yaml 238 | index 8b01bb8..ce9afa9 100644 239 | --- a/delegations.yaml 240 | +++ b/delegations.yaml 241 | @@ -113,5 +113,6 @@ delegations: 242 | RequireMFA: True 243 | Policies: 244 | - ReadOnlyAccess 245 | + - ReadS3Bucket 246 | 247 | 248 | Review proposed changes in ``dry-run`` mode:: 249 | 250 | $ awsauth delegations 251 | 252 | Implement and review changes:: 253 | 254 | $ awsauth delegations --exec 255 | $ awsauth report --roles | egrep "^Account|awsauth/ReadS3Bucket" 256 | $ aws iam list-group-policies --group-name testers 257 | $ aws iam get-group-policy --group-name testers --policy-name AllowAssumeRole-TestersRole 258 | $ aws iam get-group-policy --group-name testers --policy-name DenyAssumeRole-TestersRole 259 | 260 | 261 | Modify a custom policy 262 | ********************** 263 | 264 | Files to edit: 265 | 266 | - custom_policies.yaml 267 | 268 | Example Diff:: 269 | 270 | ~/.awsorgs/spec.d> git diff 271 | diff --git a/custom_policies.yaml b/custom_policies.yaml 272 | index a428164..7efe46b 100644 273 | --- a/custom_policies.yaml 274 | +++ b/custom_policies.yaml 275 | @@ -131,3 +131,5 @@ custom_policies: 276 | Resource: 277 | - arn:aws:s3:::my_bucket 278 | - arn:aws:s3:::my_bucket/* 279 | + - arn:aws:s3:::my_other_bucket 280 | + - arn:aws:s3:::my_other_bucket/* 281 | 282 | Review proposed changes in ``dry-run`` mode:: 283 | 284 | $ awsauth delegations 285 | 286 | Implement and review changes:: 287 | 288 | $ awsauth delegations --exec 289 | $ awsauth report --roles --full | grep -A12 awsauth/ReadS3Bucket 290 | 291 | 292 | Create a policy set and apply it to the delegation 293 | ************************************************** 294 | 295 | Files to edit: 296 | 297 | - policy_sets.yaml 298 | 299 | - create a new policy_set: 300 | 301 | - use the same policies as are listed in the delegation 302 | - include a tag and value of your choice 303 | 304 | - delegations.yaml 305 | 306 | - delete the ``Policies`` attribute from the delegation 307 | - set the ``PolicySet`` attribute to the name of your new policy set 308 | 309 | Example Diff:: 310 | 311 | ~/.awsorgs/spec.d> git diff 312 | diff --git a/policy_sets.yaml b/policy_sets.yaml 313 | index ae4c72d..1d991d2 100644 314 | --- a/policy_sets.yaml 315 | +++ b/policy_sets.yaml 316 | @@ -18,6 +18,14 @@ policy_sets: 317 | 318 | +- Name: TesterPolicySet 319 | + Description: Access for testers 320 | + Tags: 321 | + - Key: jobfunctionrole 322 | + Value: True 323 | + Policies: 324 | + - ReadOnlyAccess 325 | + - ReadS3Bucket 326 | 327 | diff --git a/delegations.yaml b/delegations.yaml 328 | index 1ae3245..4d571e9 100644 329 | --- a/delegations.yaml 330 | +++ b/delegations.yaml 331 | @@ -101,3 +101,14 @@ delegations: 332 | 333 | - RoleName: TestersRole 334 | Ensure: present 335 | Description: testing cross account delegation 336 | TrustingAccount: 337 | TrustedGroup: testers 338 | RequireMFA: True 339 | - Policies: 340 | - - ReadOnlyAccess 341 | - - ReadS3Bucket 342 | + PolicySet: TesterPolicySet 343 | 344 | 345 | Review proposed changes in ``dry-run`` mode:: 346 | 347 | $ awsauth delegations 348 | 349 | Implement and review changes:: 350 | 351 | $ awsauth delegations --exec 352 | $ aws iam list-role-tags --role-name TestersRole 353 | 354 | 355 | Modify attributes of a policy set 356 | ********************************* 357 | 358 | Files to edit: policy_sets.yaml 359 | 360 | - modify attributes: 361 | 362 | - Tags 363 | - Policies 364 | 365 | Example Diff:: 366 | 367 | :~/.awsorgs/spec.d> git diff policy-sets-spec.yml 368 | diff --git a/policy-sets-spec.yml b/policy-sets-spec.yml 369 | index 6f557d2..4c35965 100644 370 | --- a/policy-sets-spec.yml 371 | +++ b/policy-sets-spec.yml 372 | @@ -163,16 +163,15 @@ policy_sets: 373 | - Name: Developer 374 | Description: > 375 | Access to application services, but cannot manage IAM users/groups, 376 | create Route53 HostedZones, or manage inter VPC routing. 377 | Tags: 378 | - - Key: compliance 379 | - Value: IS3 380 | + - Key: job_function 381 | + Value: Developer 382 | Policies: 383 | - SystemAdministrator 384 | - DatabaseAdministrator 385 | - PowerUserAccess 386 | + - ReadOnlyAccess 387 | 388 | 389 | Review proposed changes in ``dry-run`` mode:: 390 | 391 | $ awsauth delegations 392 | 393 | Implement and review changes:: 394 | 395 | $ awsauth delegations --exec 396 | $ aws iam get-role --role-name Developer 397 | $ aws iam list-attached-role-policies --role-name Developer 398 | 399 | 400 | Delete the delegation from all accounts 401 | *************************************** 402 | 403 | Files to edit: delegations.yaml 404 | 405 | - set ``Ensure: absent`` 406 | 407 | Example Diff:: 408 | 409 | ~/.awsorgs/spec.d> git diff 410 | diff --git a/delegations.yaml b/delegations.yaml 411 | index 2b050da..b6892d1 100644 412 | --- a/delegations.yaml 413 | +++ b/delegations.yaml 414 | @@ -67,14 +67,10 @@ delegations: 415 | - ViewBilling 416 | 417 | - RoleName: TestersRole 418 | - Ensure: present 419 | + Ensure: absent 420 | Description: testing cross account delegation 421 | TrustingAccount: ALL 422 | ExcludeAccounts: 423 | - blee-poc 424 | - blee-dev 425 | - blee-prod 426 | 427 | Review proposed changes in ``dry-run`` mode:: 428 | 429 | $ awsauth delegations 430 | 431 | Implement and review changes:: 432 | 433 | $ awsauth delegations --exec 434 | $ awsauth report --roles | egrep "^Account|role/awsauth/ReadS3Bucket" 435 | $ aws iam list-group-policies --group-name testers 436 | 437 | 438 | -------------------------------------------------------------------------------- /awsorgs/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions used by the various awsorgs modules""" 2 | 3 | import os 4 | import sys 5 | import re 6 | import pkg_resources 7 | import difflib 8 | import threading 9 | try: 10 | import queue 11 | except ImportError: 12 | import Queue as queue 13 | 14 | import boto3 15 | from botocore.exceptions import ClientError 16 | import yaml 17 | import logging 18 | 19 | 20 | S3_BUCKET_PREFIX = 'awsorgs' 21 | S3_OBJECT_KEY = 'deployed_accounts.yaml' 22 | 23 | 24 | def get_s3_bucket_name(prefix=S3_BUCKET_PREFIX): 25 | """ 26 | Generate an s3 bucket name based on a name prefix and the aws account ig 27 | """ 28 | sts_client = boto3.client('sts') 29 | account_id = sts_client.get_caller_identity()['Account'] 30 | return '-'.join([prefix, account_id]) 31 | 32 | 33 | def lookup(dlist, lkey, lvalue, rkey=None): 34 | """ 35 | Use a known key:value pair to lookup a dictionary in a list of 36 | dictionaries. Return the dictonary or None. If rkey is provided, 37 | return the value referenced by rkey or None. If more than one 38 | dict matches, raise an error. 39 | args: 40 | dlist: lookup table - a list of dictionaries 41 | lkey: name of key to use as lookup criteria 42 | lvalue: value to use as lookup criteria 43 | rkey: (optional) name of key referencing a value to return 44 | """ 45 | items = [d for d in dlist 46 | if lkey in d 47 | and d[lkey] == lvalue] 48 | if not items: 49 | return None 50 | if len(items) > 1: 51 | raise RuntimeError( 52 | "Data Error: lkey: {}, lvalue: {} - lookup matches multiple items in dlist".format(lkey, lvalue) 53 | ) 54 | if rkey: 55 | if rkey in items[0]: 56 | return items[0][rkey] 57 | return None 58 | return items[0] 59 | 60 | def search_spec(spec, search_key, recurse_key): 61 | """ 62 | Recursively scans spec structure and returns a list of values 63 | keyed with 'search_key' or and empty list. Assumes values 64 | are either list or str. 65 | """ 66 | value = [] 67 | if search_key in spec and spec[search_key]: 68 | if isinstance(spec[search_key], str): 69 | value.append(spec[search_key]) 70 | else: 71 | value += spec[search_key] 72 | if recurse_key in spec and spec[recurse_key]: 73 | for child_spec in spec[recurse_key]: 74 | value += search_spec(child_spec, search_key, recurse_key) 75 | return sorted(value) 76 | 77 | 78 | def ensure_absent(spec): 79 | """ 80 | test if an 'Ensure' key is set to absent in dictionary 'spec' 81 | """ 82 | if 'Ensure' in spec and spec['Ensure'] == 'absent': return True 83 | return False 84 | 85 | 86 | def munge_path(default_path, spec): 87 | """ 88 | Return formated 'Path' attribute for use in iam client calls. 89 | Unless specified path is fully qualified (i.e. starts with '/'), 90 | prepend the 'default_path'. 91 | """ 92 | if 'Path' in spec and spec['Path']: 93 | if spec['Path'][0] == '/': 94 | if spec['Path'][-1] != '/': 95 | return spec['Path'] + '/' 96 | return spec['Path'] 97 | return "/%s/%s/" % (default_path, spec['Path']) 98 | return "/%s/" % default_path 99 | 100 | 101 | def get_logger(args): 102 | """ 103 | Setup logging.basicConfig from args. 104 | Return logging.Logger object. 105 | """ 106 | # log level 107 | log_level = logging.INFO 108 | if args['--debug']: 109 | log_level = logging.DEBUG 110 | if args['--quiet']: 111 | log_level = logging.CRITICAL 112 | # log format 113 | log_format = '%(name)s: %(levelname)-9s%(message)s' 114 | if args['report']: 115 | log_format = '%(message)s' 116 | if args['--debug'] == 1: 117 | log_format = '%(name)s: %(levelname)-9s%(funcName)s(): %(message)s' 118 | if (not args['--exec'] and not args['report']): 119 | log_format = '[dryrun] %s' % log_format 120 | if not args['--debug'] == 2: 121 | logging.getLogger('botocore').propagate = False 122 | logging.getLogger('boto3').propagate = False 123 | logging.basicConfig(stream=sys.stdout, format=log_format, level=log_level) 124 | log = logging.getLogger(__name__) 125 | return log 126 | 127 | 128 | def valid_account_id(log, account_id): 129 | """Validate account Id is a 12 digit string""" 130 | if not isinstance(account_id, str): 131 | log.error("supplied account id {} is not a string".format(account_id)) 132 | return False 133 | id_re = re.compile(r'^\d{12}$') 134 | if not id_re.match(account_id): 135 | log.error("supplied account id '{}' must be a 12 digit number".format(account_id)) 136 | return False 137 | return True 138 | 139 | 140 | def get_root_id(org_client): 141 | """ 142 | Query deployed AWS Organization for its Root ID. 143 | """ 144 | roots = org_client.list_roots()['Roots'] 145 | if len(roots) >1: 146 | raise RuntimeError("org_client.list_roots returned multiple roots.") 147 | return roots[0]['Id'] 148 | 149 | 150 | def validate_master_id(org_client, spec): 151 | """ 152 | Don't mangle the wrong org by accident 153 | """ 154 | master_account_id = org_client.describe_organization( 155 | )['Organization']['MasterAccountId'] 156 | if master_account_id != spec['master_account_id']: 157 | errmsg = ("The Organization Master Account Id '%s' does not match the " 158 | "'master_account_id' set in the spec-file" % master_account_id) 159 | raise RuntimeError(errmsg) 160 | return 161 | 162 | 163 | def queue_threads(log, sequence, func, f_args=(), thread_count=20): 164 | """generalized abstraction for running queued tasks in a thread pool""" 165 | 166 | def worker(*args): 167 | log.debug('%s: q.empty: %s' % (threading.current_thread().name, q.empty())) 168 | while not q.empty(): 169 | log.debug('%s: task: %s' % (threading.current_thread().name, func)) 170 | item = q.get() 171 | log.debug('%s: processing item: %s' % (threading.current_thread().name, item)) 172 | func(item, *args) 173 | q.task_done() 174 | 175 | q = queue.Queue() 176 | for item in sequence: 177 | log.debug('queuing item: %s' % item) 178 | q.put(item) 179 | log.debug('queue length: %s' % q.qsize()) 180 | for i in range(thread_count): 181 | t = threading.Thread(target=worker, args=f_args) 182 | t.setDaemon(True) 183 | t.start() 184 | q.join() 185 | 186 | 187 | def get_assume_role_credentials(account_id, role_name, region_name=None): 188 | """ 189 | Get temporary sts assume_role credentials for account. 190 | """ 191 | role_arn = "arn:aws:iam::%s:role/%s" % (account_id, role_name) 192 | role_session_name = account_id + '-' + role_name.split('/')[-1] 193 | sts_client = boto3.client('sts') 194 | 195 | if account_id == sts_client.get_caller_identity()['Account']: 196 | return dict( 197 | aws_access_key_id=None, 198 | aws_secret_access_key=None, 199 | aws_session_token=None, 200 | region_name=None) 201 | else: 202 | try: 203 | credentials = sts_client.assume_role( 204 | RoleArn=role_arn, 205 | RoleSessionName=role_session_name 206 | )['Credentials'] 207 | except ClientError as e: 208 | if e.response['Error']['Code'] == 'AccessDenied': 209 | errmsg = ('cannot assume role %s in account %s' % 210 | (role_name, account_id)) 211 | return RuntimeError(errmsg) 212 | return dict( 213 | aws_access_key_id=credentials['AccessKeyId'], 214 | aws_secret_access_key=credentials['SecretAccessKey'], 215 | aws_session_token=credentials['SessionToken'], 216 | region_name=region_name) 217 | 218 | 219 | def scan_deployed_accounts(log, org_client): 220 | """ 221 | Query AWS Organization for deployed accounts. 222 | Returns a list of dictionary. 223 | """ 224 | log.debug('running') 225 | accounts = org_client.list_accounts() 226 | deployed_accounts = accounts['Accounts'] 227 | while 'NextToken' in accounts and accounts['NextToken']: 228 | accounts = org_client.list_accounts(NextToken=accounts['NextToken']) 229 | deployed_accounts += accounts['Accounts'] 230 | # only return accounts that have an 'Name' key 231 | return [d for d in deployed_accounts if 'Name' in d ] 232 | 233 | 234 | def scan_created_accounts(log, org_client): 235 | """ 236 | Query AWS Organization for accounts with creation status of 'SUCCEEDED'. 237 | Returns a list of dictionary. 238 | """ 239 | log.debug('running') 240 | status = org_client.list_create_account_status(States=['SUCCEEDED']) 241 | created_accounts = status['CreateAccountStatuses'] 242 | while 'NextToken' in status and status['NextToken']: 243 | status = org_client.list_create_account_status( 244 | States=['SUCCEEDED'], 245 | NextToken=status['NextToken']) 246 | created_accounts += status['CreateAccountStatuses'] 247 | return created_accounts 248 | 249 | 250 | def get_account_aliases(log, deployed_accounts, role): 251 | """ 252 | Return dict of {Id:Alias} for all deployed accounts. 253 | 254 | role:: name of IAM role to assume to query all deployed accounts. 255 | """ 256 | # worker function for threading 257 | def get_account_alias(account, log, role, aliases): 258 | if account['Status'] == 'ACTIVE': 259 | credentials = get_assume_role_credentials(account['Id'], role) 260 | if isinstance(credentials, RuntimeError): 261 | log.error(credentials) 262 | return 263 | iam_client = boto3.client('iam', **credentials) 264 | response = iam_client.list_account_aliases()['AccountAliases'] 265 | if response: 266 | aliases[account['Id']] = response[0] 267 | # call workers 268 | aliases = {} 269 | queue_threads(log, deployed_accounts, get_account_alias, 270 | f_args=(log, role, aliases), thread_count=10) 271 | log.debug(yamlfmt(aliases)) 272 | return aliases 273 | 274 | 275 | def merge_aliases(log, deployed_accounts, aliases): 276 | """ 277 | Merge account aliases into deployed_accounts lookup table. 278 | """ 279 | for account in deployed_accounts: 280 | account['Alias'] = aliases.get(account['Id'], '') 281 | log.debug(account) 282 | return deployed_accounts 283 | 284 | 285 | def string_differ(string1, string2): 286 | """Returns the diff of 2 strings""" 287 | diff = difflib.ndiff( 288 | string1.splitlines(keepends=True), 289 | string2.splitlines(keepends=True), 290 | ) 291 | return ''.join(list(diff)) 292 | 293 | 294 | def yamlfmt(dict_obj): 295 | """Convert a dictionary object into a yaml formated string""" 296 | return yaml.dump(dict_obj, default_flow_style=False) 297 | 298 | 299 | def overbar(string): 300 | """ 301 | Returns string preceeded by an overbar of the same length: 302 | >>> print(overbar('blee')) 303 | ____ 304 | blee 305 | """ 306 | return "%s\n%s" % ('_' * len(string), string) 307 | 308 | 309 | def report_maker(log, accounts, role, query_func, report_header=None, **qf_args): 310 | """ 311 | Generate a report by running a arbitrary query function in each account. 312 | The query function must return a list of strings. 313 | """ 314 | # Thread worker function to gather report for each account 315 | def make_account_report(account, report, role): 316 | messages = [] 317 | messages.append(overbar("Account: %s" % account['Name'])) 318 | credentials = get_assume_role_credentials(account['Id'], role) 319 | if isinstance(credentials, RuntimeError): 320 | messages.append(credentials) 321 | else: 322 | messages += query_func(credentials, **qf_args) 323 | report[account['Name']] = messages 324 | # gather report data from accounts 325 | report = {} 326 | queue_threads( 327 | log, accounts, 328 | make_account_report, 329 | f_args=(report, role), 330 | thread_count=10) 331 | # process the reports 332 | if report_header: 333 | log.info("\n\n%s" % overbar(report_header)) 334 | for account, messages in sorted(report.items()): 335 | for msg in messages: 336 | log.info(msg) 337 | 338 | 339 | def get_iam_objects(iam_client_function, object_key, f_args=dict()): 340 | """ 341 | users = get_iam_objects(iam_client.list_users, 'Users') 342 | """ 343 | iam_objects = [] 344 | response = iam_client_function(**f_args) 345 | iam_objects += response[object_key] 346 | if 'IsTruncated' in response: 347 | while response['IsTruncated']: 348 | response = iam_client_function(Marker=response['Marker'],**f_args) 349 | iam_objects += response[object_key] 350 | return iam_objects 351 | 352 | 353 | 354 | -------------------------------------------------------------------------------- /awsorgs/loginprofile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Manage AWS IAM user login profile. 3 | 4 | Usage: 5 | awsloginprofile USER [--config FILE] 6 | [--master-account-id ID] 7 | [--auth-account-id ID] 8 | [--org-access-role ROLE] 9 | [--report] 10 | [--new | --reset | --reenable] 11 | [--no-email] 12 | [--disable] 13 | [--disable-expired] 14 | [--opt-ttl HOURS] 15 | [--password PASSWORD] 16 | [-q] [-d|-dd] 17 | awsloginprofile (--help|--version) 18 | 19 | Options: 20 | USER Name of IAM user. 21 | -h, --help Show this help message and exit. 22 | -V, --version Display version info and exit. 23 | --config FILE AWS Org config file in yaml format. 24 | --master-account-id ID AWS account Id of the Org master account. 25 | --auth-account-id ID AWS account Id of the authentication account. 26 | --org-access-role ROLE IAM role for traversing accounts in the Org. 27 | --report Print user login profile report. this is the default 28 | --new Create new login profile. 29 | --reset Reset password for existing login profile. 30 | --no-email Do not email user when (re)setting login profile. 31 | --disable Delete existing login profile, disable access keys. 32 | --disable-expired Delete profile if one-time-password exceeds --opt-ttl. 33 | --reenable Recreate login profile, reactivate access keys. 34 | --opt-ttl HOURS One-time-password time to live in hours [default: 24]. 35 | --password PASSWORD Supply password, do not require user to reset. 36 | -q, --quiet Repress log output. 37 | -d, --debug Increase log level to 'DEBUG'. 38 | -dd Include botocore and boto3 logs in log stream. 39 | 40 | """ 41 | 42 | 43 | import os 44 | import sys 45 | import yaml 46 | import logging 47 | from string import Template 48 | import datetime 49 | import smtplib 50 | from email.message import EmailMessage 51 | 52 | 53 | import boto3 54 | from botocore.exceptions import ClientError 55 | from docopt import docopt 56 | from passwordgenerator import pwgenerator 57 | 58 | 59 | import awsorgs 60 | from awsorgs.utils import * 61 | from awsorgs.spec import * 62 | from awsorgs.reports import * 63 | 64 | 65 | # Relative path within awsorgs project to template file used by prep_email() 66 | EMAIL_TEMPLATE = 'data/email_template' 67 | 68 | 69 | def utcnow(): 70 | return datetime.datetime.now(datetime.timezone.utc) 71 | 72 | 73 | def get_user_name(): 74 | """ 75 | Returns the IAM user_name of the calling identidy (i.e. you) 76 | """ 77 | sts = boto3.client('sts') 78 | return sts.get_caller_identity()['Arn'].split('/')[-1] 79 | 80 | 81 | def list_delegations(log, user, deployed_accounts): 82 | """ 83 | Return list of assume role resource arns for user. Obtain these by 84 | parsing in-line group policies for each group the user is a member of. 85 | 86 | the policies we care about match one of two patterns: 87 | AllowAssumeRole- 88 | DenyAssumeRole- 89 | 90 | we assemble a list of allowed role arns, then we remove any of the 91 | denied arns. 92 | 93 | if the account_id field of a AllowAssumeRole- matches the 94 | glob ('*') char, we generate the list of allowed role arns - one for 95 | every 'ACTIVE' account. 96 | """ 97 | groups = list(user.groups.all()) 98 | role_arns = [] 99 | for group in user.groups.all(): 100 | 101 | allow_policies = [ 102 | p for p in list(group.policies.all()) 103 | if p.policy_name.startswith('AllowAssumeRole') 104 | ] 105 | for allow_policy in allow_policies: 106 | allow_arns = allow_policy.policy_document['Statement'][0]['Resource'] 107 | if isinstance(allow_arns, str) and '*' in allow_arns: 108 | head, sep, tail = allow_arns.partition('*') 109 | allow_arns = [] 110 | for account in deployed_accounts: 111 | if account['Status'] == 'ACTIVE': 112 | allow_arns.append(head + account['Id'] + tail) 113 | role_arns += allow_arns 114 | 115 | deny_policies = [ 116 | p for p in list(group.policies.all()) 117 | if p.name.startswith('DenyAssumeRole') 118 | ] 119 | for deny_policy in deny_policies: 120 | deny_arns = deny_policy.policy_document['Statement'][0]['Resource'] 121 | deny_account_ids = [arn.split(':')[4] for arn in deny_arns] 122 | for id in deny_account_ids: 123 | for arn in role_arns: 124 | if id in arn: 125 | role_arns.remove(arn) 126 | return role_arns 127 | 128 | 129 | def format_delegation_table(delegation_arns, deployed_accounts): 130 | """Generate formatted list of delegation attributes as printable string""" 131 | template = " {} {}{}{}\n" 132 | delegation_string = template.format('Account Id ', 'Alias', ' '*19, 'Role') 133 | for assume_role_arn in delegation_arns: 134 | account_id = assume_role_arn.split(':')[4] 135 | alias = lookup(deployed_accounts, 'Id', account_id, 'Alias') 136 | if alias is None: 137 | alias = str() 138 | spacer = (24 - len(alias)) * ' ' 139 | delegation_string += template.format(account_id, alias, spacer, 140 | assume_role_arn.partition('role/')[2]) 141 | return delegation_string 142 | 143 | 144 | def user_report(log, deployed_accounts, user, login_profile): 145 | """Generate report of IAM user's login profile, password usage, and 146 | assume_role delegations for any groups user is member of. 147 | """ 148 | spacer = '{:<24}{}' 149 | log.info('\n') 150 | log.info(spacer.format('User:', user.name)) 151 | log.info(spacer.format('Arn:', user.arn)) 152 | log.info(spacer.format('User Id:', user.user_id)) 153 | log.info(spacer.format('User created:', user.create_date)) 154 | if login_profile: 155 | log.info(spacer.format('Login profile created:', login_profile.create_date)) 156 | log.info(spacer.format('Passwd reset required:', login_profile.password_reset_required)) 157 | if login_profile.password_reset_required: 158 | log.info(spacer.format( 159 | 'One-time-passwd age:', 160 | utcnow() - login_profile.create_date, 161 | )) 162 | else: 163 | log.info(spacer.format('Password last used:', user.password_last_used)) 164 | else: 165 | log.info(spacer.format('User login profile:', login_profile)) 166 | assume_role_arns = list_delegations(log, user, deployed_accounts) 167 | if assume_role_arns: 168 | log.info('Delegations:\n{}'.format( 169 | format_delegation_table(assume_role_arns, deployed_accounts) 170 | )) 171 | 172 | 173 | def validate_user(user_name, credentials=None): 174 | """Return a valid IAM User object""" 175 | if credentials: 176 | iam = boto3.resource('iam', **credentials) 177 | else: 178 | iam = boto3.resource('iam') 179 | user = iam.User(user_name) 180 | try: 181 | user.load() 182 | except ClientError as e: 183 | if e.response['Error']['Code'] == 'NoSuchEntity': 184 | return 185 | return user 186 | 187 | 188 | def validate_login_profile(user): 189 | """Return a valid IAM LoginProfile object""" 190 | login_profile = user.LoginProfile() 191 | try: 192 | login_profile.load() 193 | except ClientError as e: 194 | if e.response['Error']['Code'] == 'NoSuchEntity': 195 | return 196 | return login_profile 197 | 198 | 199 | def munge_passwd(passwd=None): 200 | """Return new 'passwd' string and boolean 'require_reset'. 201 | If passwd provided, set 'require_reset' to False. 202 | """ 203 | if passwd: 204 | require_reset = False 205 | else: 206 | passwd = pwgenerator.generate() 207 | require_reset = True 208 | return passwd, require_reset 209 | 210 | 211 | def create_profile(log, user, passwd, require_reset): 212 | log.debug('creating login profile for user %s' % user.name) 213 | return user.create_login_profile( 214 | Password=passwd, 215 | PasswordResetRequired=require_reset, 216 | ) 217 | 218 | 219 | def reset_profile(log, user, login_profile, passwd, require_reset): 220 | """Reset IAM user passwd by deleting and recreating login profile. 221 | This ensures the password creation date gets reset when updating a password. 222 | """ 223 | if login_profile: 224 | log.debug('resetting login profile for user %s' % user.name) 225 | login_profile.delete() 226 | return login_profile.create( 227 | Password=passwd, 228 | PasswordResetRequired=require_reset 229 | ) 230 | else: 231 | log.error("user '%s' has no login profile" % user.name) 232 | sys.exit(1) 233 | 234 | def delete_profile(log, user, login_profile): 235 | if login_profile: 236 | log.info('deleting login profile for user %s' % user.name) 237 | login_profile.delete() 238 | else: 239 | log.warn("user '%s' has no login profile" % user.name) 240 | 241 | 242 | def set_access_key_status(log, user, enable=True): 243 | """Enable or disable an IAM user's access keys""" 244 | for key in user.access_keys.all(): 245 | if enable and key.status == 'Inactive': 246 | log.info('enabling access key %s for user %s' % 247 | (key.access_key_id, user.name)) 248 | key.activate() 249 | elif not enable and key.status == 'Active': 250 | log.info('disabling access key %s for user %s' % 251 | (key.access_key_id, user.name)) 252 | key.deactivate() 253 | 254 | 255 | def onetime_passwd_expired(log, user, login_profile, hours): 256 | """Test if initial one-time-only password is expired""" 257 | if login_profile and login_profile.password_reset_required: 258 | log.debug('now: %s' % utcnow().isoformat()) 259 | log.debug('ttl: %s' % datetime.timedelta(hours=hours)) 260 | log.debug('delta: %s' % (utcnow() - login_profile.create_date)) 261 | return (utcnow() - login_profile.create_date) > datetime.timedelta(hours=hours) 262 | return False 263 | 264 | 265 | def prep_email(log, aliases, deployed_accounts, user, passwd): 266 | """Generate email body from template""" 267 | log.debug("loading file: '%s'" % EMAIL_TEMPLATE) 268 | trusted_id=boto3.client('sts').get_caller_identity()['Account'] 269 | if aliases: 270 | trusted_account = aliases[trusted_id] 271 | else: 272 | trusted_account = trusted_id 273 | assume_role_arns = list_delegations(log, user, deployed_accounts) 274 | log.debug('assume_role_arns: %s' % assume_role_arns) 275 | template = os.path.abspath(pkg_resources.resource_filename(__name__, EMAIL_TEMPLATE)) 276 | mapping = dict( 277 | user_name=user.name, 278 | onetimepw=passwd, 279 | trusted_account=trusted_account, 280 | delegations=format_delegation_table(assume_role_arns, deployed_accounts), 281 | ) 282 | with open(template) as tpl: 283 | return Template(tpl.read()).substitute(mapping) 284 | 285 | 286 | def build_email_message(user, message_body, spec): 287 | msg = EmailMessage() 288 | msg.set_content(message_body) 289 | msg['Subject'] = 'login profile' 290 | msg['To'] = lookup(spec['users'], 'Name', user.name, 'Email') 291 | msg['From'] = spec['org_admin_email'] 292 | return msg 293 | 294 | def send_email(msg, smtp_server): 295 | s = smtplib.SMTP(smtp_server) 296 | s.send_message(msg) 297 | s.quit() 298 | 299 | 300 | def handle_email(log, args, spec, aliases, deployed_accounts, user, passwd): 301 | message_body = prep_email(log, aliases, deployed_accounts, user, passwd) 302 | if args['--no-email']: 303 | print(message_body) 304 | else: 305 | msg = build_email_message(user, message_body, spec) 306 | send_email(msg, spec['default_smtp_server']) 307 | 308 | 309 | def main(): 310 | args = docopt(__doc__, version=awsorgs.__version__) 311 | # HACK ALERT! 312 | # set '--exec' and 'report' args to make get_logger() happy 313 | args['--exec'] = True 314 | if not (args['--new'] 315 | or args['--reset'] 316 | or args['--disable'] 317 | or args['--disable-expired'] 318 | or args['--reenable']): 319 | args['report'] = True 320 | else: 321 | args['report'] = False 322 | log = get_logger(args) 323 | log.debug("%s: args:\n%s" % (__name__, args)) 324 | args = load_config(log, args) 325 | spec = validate_spec(log, args) 326 | 327 | user = validate_user(args['USER']) 328 | if not user: 329 | log.critical('no such user: %s' % args['USER']) 330 | sys.exit(1) 331 | login_profile = validate_login_profile(user) 332 | passwd, require_reset = munge_passwd(args['--password']) 333 | org_credentials = get_assume_role_credentials( 334 | args['--master-account-id'], 335 | args['--org-access-role']) 336 | if isinstance(org_credentials, RuntimeError): 337 | log.critical(org_credentials) 338 | sys.exit(1) 339 | org_client = boto3.client('organizations', **org_credentials) 340 | deployed_accounts = scan_deployed_accounts(log, org_client) 341 | aliases = get_account_aliases(log, deployed_accounts, args['--org-access-role']) 342 | deployed_accounts = merge_aliases(log, deployed_accounts, aliases) 343 | log.debug(aliases) 344 | 345 | if args['--new']: 346 | if not login_profile: 347 | login_profile = create_profile(log, user, passwd, require_reset) 348 | handle_email(log, args, spec, aliases, deployed_accounts, user, passwd) 349 | else: 350 | log.warn("login profile for user '%s' already exists" % user.name) 351 | user_report(log, deployed_accounts, user, login_profile) 352 | 353 | elif args['--reset']: 354 | login_profile = reset_profile(log, user, login_profile, passwd, require_reset) 355 | handle_email(log, args, spec, aliases, deployed_accounts, user, passwd) 356 | 357 | elif args['--disable']: 358 | delete_profile(log, user, login_profile) 359 | set_access_key_status(log, user, False) 360 | 361 | elif args['--disable-expired']: 362 | if onetime_passwd_expired(log, user, login_profile, int(args['--opt-ttl'])): 363 | delete_profile(log, user, login_profile) 364 | 365 | elif args['--reenable']: 366 | if not login_profile: 367 | login_profile = create_profile(log, user, passwd, require_reset) 368 | handle_email(log, args, spec, aliases, deployed_accounts, user, passwd) 369 | else: 370 | log.warn("login profile for user '%s' already exists" % user.name) 371 | set_access_key_status(log, user, True) 372 | 373 | else: 374 | user_report(log, deployed_accounts, user, login_profile) 375 | 376 | 377 | if __name__ == "__main__": 378 | main() 379 | -------------------------------------------------------------------------------- /awsorgs/reports.py: -------------------------------------------------------------------------------- 1 | """ 2 | Report maker utility and query functions 3 | 4 | Todo: 5 | allow reporting on single account or short list of accounts 6 | substitute account alias for account id in reports 7 | 8 | """ 9 | 10 | import io 11 | import csv 12 | from awsorgs.utils import * 13 | 14 | 15 | # Report_maker utilities 16 | 17 | def overbar(string): 18 | """ 19 | Returns string preceeded by an overbar of the same length: 20 | >>> print(overbar('blee')) 21 | ____ 22 | blee 23 | """ 24 | return "%s\n%s" % ('_' * len(string), string) 25 | 26 | 27 | def report_maker(log, accounts, role, query_func, report_header=None, **qf_args): 28 | """ 29 | Generate a report by running a arbitrary query function in each account. 30 | The query function must return a list of strings. 31 | """ 32 | # Thread worker function to gather report for each account 33 | def make_account_report(account, report, role): 34 | messages = [] 35 | messages.append(overbar("Account: %s" % account['Name'])) 36 | credentials = get_assume_role_credentials(account['Id'], role) 37 | if isinstance(credentials, RuntimeError): 38 | messages.append(credentials) 39 | else: 40 | messages += query_func(credentials, **qf_args) 41 | report[account['Name']] = messages 42 | # gather report data from accounts 43 | report = {} 44 | queue_threads( 45 | log, accounts, 46 | make_account_report, 47 | f_args=(report, role), 48 | thread_count=10) 49 | # process the reports 50 | if report_header: 51 | log.info("\n\n%s" % overbar(report_header)) 52 | for account, messages in sorted(report.items()): 53 | for msg in messages: 54 | log.info(msg) 55 | 56 | 57 | # report_maker query functions 58 | 59 | def user_group_report(credentials, verbose=False): 60 | """ 61 | A report_maker query function. 62 | Reports IAM users and Groups in an account. 63 | 64 | ISSUE: report access keys, ssh keys, mfa devices, http users 65 | 66 | """ 67 | messages = [] 68 | iam_client = boto3.client('iam', **credentials) 69 | 70 | user_info = [] 71 | users = get_iam_objects(iam_client.list_users, 'Users') 72 | for u in users: 73 | if verbose: 74 | user_info.append(u) 75 | else: 76 | user_info.append(u['Arn']) 77 | if user_info: 78 | messages.append(yamlfmt(dict(Users=user_info))) 79 | 80 | group_info = [] 81 | groups = get_iam_objects(iam_client.list_groups, 'Groups') 82 | for g in groups: 83 | if verbose: 84 | group_info.append(g) 85 | else: 86 | group_info.append(g['Arn']) 87 | if group_info: 88 | messages.append(yamlfmt(dict(Groups=group_info))) 89 | #if groups: 90 | # messages.append("Groups:") 91 | # if verbose: 92 | # messages.append(yamlfmt(groups)) 93 | # else: 94 | # messages += [" %s" % group['Arn'] for group in groups] 95 | return messages 96 | 97 | 98 | def credentials_report(credentials): 99 | """ 100 | A report_maker query function. 101 | IAM Credential report in an account 102 | 103 | ISSUES: 104 | Clean up exception handling: 105 | botocore.errorfactory.CredentialReportNotPresentException: An error occurred (ReportNotPresent) when calling the GetCredentialReport operation: Unknown 106 | """ 107 | 108 | messages = [] 109 | iam_client = boto3.client('iam', **credentials) 110 | try: 111 | response = iam_client.get_credential_report() 112 | except Exception as e: 113 | response = iam_client.generate_credential_report() 114 | messages.append(yamlfmt(response)) 115 | return messages 116 | 117 | report_file_object = io.StringIO(response['Content'].decode()) 118 | reader = csv.DictReader(report_file_object) 119 | user_info = [] 120 | for row in reader: 121 | user = dict() 122 | for key in reader.fieldnames: 123 | user['UserName'] = row['user'] 124 | user['Arn'] = row['arn'] 125 | if (key not in ['user', 'arn'] and 126 | row[key] not in ['N/A', 'not_supported', 'no_information', 'false']): 127 | user[key] = row[key] 128 | user_info.append(user) 129 | 130 | if user_info: 131 | messages.append(yamlfmt(dict(Users=user_info))) 132 | return messages 133 | 134 | 135 | def role_report(credentials, verbose=False): 136 | """ 137 | A report_maker query function. 138 | Reports IAM custom policies and roles in an account. 139 | """ 140 | messages = [] 141 | iam_client = boto3.client('iam', **credentials) 142 | iam_resource = boto3.resource('iam', **credentials) 143 | 144 | policy_info = [] 145 | custom_policies = get_iam_objects(iam_client.list_policies, 'Policies', 146 | dict(Scope='Local')) 147 | for p in custom_policies: 148 | if verbose: 149 | policy_version_id = iam_resource.Policy(p['Arn']).default_version_id 150 | policy_info.append(dict( 151 | Arn=p['Arn'], 152 | Statement=iam_resource.PolicyVersion( 153 | p['Arn'], 154 | policy_version_id 155 | ).document['Statement'], 156 | )) 157 | else: 158 | policy_info.append(p['Arn']) 159 | if policy_info: 160 | messages.append(yamlfmt(dict(CustomPolicies=policy_info))) 161 | 162 | role_info = [] 163 | roles = get_iam_objects(iam_client.list_roles, 'Roles') 164 | for r in roles: 165 | role = iam_resource.Role(r['RoleName']) 166 | if verbose: 167 | role_info.append(dict( 168 | Arn=role.arn, 169 | Statement=role.assume_role_policy_document['Statement'], 170 | AttachedPolicies=[p.policy_name for p in list(role.attached_policies.all())], 171 | )) 172 | else: 173 | role_info.append(role.arn) 174 | if role_info: 175 | messages.append(yamlfmt(dict(Roles=role_info))) 176 | return messages 177 | 178 | 179 | def account_authorization_report(credentials, verbose=False): 180 | """ 181 | A report_maker query function. 182 | IAM Account Authorization Reporting 183 | 184 | """ 185 | messages = [] 186 | iam_client = boto3.client('iam', **credentials) 187 | 188 | user_info = [] 189 | users = get_iam_objects( 190 | iam_client.get_account_authorization_details, 191 | 'UserDetailList', 192 | dict(Filter=['User'])) 193 | for u in users: 194 | if verbose: 195 | user_info.append(u) 196 | else: 197 | user_info.append(u['Arn']) 198 | if user_info: 199 | messages.append(yamlfmt(dict(Users=user_info))) 200 | 201 | group_info = [] 202 | groups = get_iam_objects( 203 | iam_client.get_account_authorization_details, 204 | 'GroupDetailList', 205 | dict(Filter=['Group'])) 206 | for u in groups: 207 | if verbose: 208 | group_info.append(u) 209 | else: 210 | group_info.append(u['Arn']) 211 | if group_info: 212 | messages.append(yamlfmt(dict(Groups=group_info))) 213 | 214 | role_info = [] 215 | roles = get_iam_objects( 216 | iam_client.get_account_authorization_details, 217 | 'RoleDetailList', 218 | dict(Filter=['Role'])) 219 | for u in roles: 220 | if verbose: 221 | role_info.append(u) 222 | else: 223 | role_info.append(u['Arn']) 224 | if role_info: 225 | messages.append(yamlfmt(dict(Roles=role_info))) 226 | 227 | policy_info = [] 228 | policies = get_iam_objects( 229 | iam_client.get_account_authorization_details, 230 | 'Policies', 231 | dict(Filter=['LocalManagedPolicy'])) 232 | for u in policies: 233 | if verbose: 234 | policy_info.append(u) 235 | else: 236 | policy_info.append(u['Arn']) 237 | if policy_info: 238 | messages.append(yamlfmt(dict(CustomPolicies=policy_info))) 239 | 240 | 241 | return messages 242 | 243 | 244 | 245 | 246 | # Obsolete resource display functions. For reference only 247 | # 248 | #display_provisioned_users(log, args, deployed, auth_spec, credentials) 249 | #display_provisioned_groups(log, args, deployed, credentials) 250 | #display_roles_in_accounts(log, args, deployed, auth_spec) 251 | 252 | 253 | def display_provisioned_users(log, args, deployed, auth_spec, credentials): 254 | """ 255 | Print report of currently deployed IAM users in Auth account. 256 | """ 257 | header = "Provisioned IAM Users in Auth Account:" 258 | overbar = '_' * len(header) 259 | log.info("\n%s\n%s\n" % (overbar, header)) 260 | if args['--full']: 261 | aliases = get_account_aliases(log, deployed['accounts'], 262 | args['--org-access-role']) 263 | for name in sorted([u['UserName'] for u in deployed['users']]): 264 | arn = lookup(deployed['users'], 'UserName', name, 'Arn') 265 | if args['--full']: 266 | user = validate_user(name, credentials) 267 | if user: 268 | login_profile = validate_login_profile(user) 269 | user_report(log, aliases, user, login_profile) 270 | else: 271 | spacer = ' ' * (12 - len(name)) 272 | log.info("%s%s\t%s" % (name, spacer, arn)) 273 | 274 | 275 | def display_provisioned_groups(log, args, deployed, credentials): 276 | """ 277 | Print report of currently deployed IAM groups in Auth account. 278 | List group memebers, attached policies and delegation assume role 279 | profiles. 280 | """ 281 | # Thread worker function to assemble lines of a group report 282 | def display_group(group_name, report, iam_resource): 283 | log.debug('group_name: %s' % group_name) 284 | messages = [] 285 | group = iam_resource.Group(group_name) 286 | members = list(group.users.all()) 287 | attached_policies = list(group.attached_policies.all()) 288 | assume_role_resources = [p.policy_document['Statement'][0]['Resource'] 289 | for p in list(group.policies.all()) if 290 | p.policy_document['Statement'][0]['Action'] == 'sts:AssumeRole'] 291 | overbar = '_' * (8 + len(group_name)) 292 | messages.append('\n%s' % overbar) 293 | messages.append("%s\t%s" % ('Name:', group_name)) 294 | messages.append("%s\t%s" % ('Arn:', group.arn)) 295 | if members: 296 | messages.append("Members:") 297 | messages.append("\n".join([" %s" % u.name for u in members])) 298 | if attached_policies: 299 | messages.append("Policies:") 300 | messages.append("\n".join([" %s" % p.arn for p in attached_policies])) 301 | if assume_role_resources: 302 | messages.append("Assume role profiles:") 303 | messages.append(" Account\tRole ARN") 304 | profiles = {} 305 | for role_arn in assume_role_resources: 306 | account_name = lookup(deployed['accounts'], 'Id', 307 | role_arn.split(':')[4], 'Name') 308 | if account_name: 309 | profiles[account_name] = role_arn 310 | for account_name in sorted(profiles.keys()): 311 | messages.append(" %s:\t%s" % (account_name, profiles[account_name])) 312 | report[group_name] = messages 313 | 314 | group_names = sorted([g['GroupName'] for g in deployed['groups']]) 315 | log.debug('group_names: %s' % group_names) 316 | header = "Provisioned IAM Groups in Auth Account:" 317 | overbar = '_' * len(header) 318 | log.info("\n\n%s\n%s" % (overbar, header)) 319 | 320 | # log report 321 | if args['--full']: 322 | # gather report data from groups 323 | report = {} 324 | iam_resource = boto3.resource('iam', **credentials) 325 | queue_threads(log, group_names, display_group, f_args=(report, iam_resource), 326 | thread_count=10) 327 | for group_name, messages in sorted(report.items()): 328 | for msg in messages: 329 | log.info(msg) 330 | else: 331 | # just print the arns 332 | log.info('') 333 | for name in group_names: 334 | arn = lookup(deployed['groups'], 'GroupName', name, 'Arn') 335 | spacer = ' ' * (12 - len(name)) 336 | log.info("%s%s\t%s" % (name, spacer, arn)) 337 | 338 | 339 | def display_roles_in_accounts(log, args, deployed, auth_spec): 340 | """ 341 | Print report of currently deployed delegation roles in each account 342 | in the Organization. 343 | We only care about AWS principals, not Service principals. 344 | """ 345 | # Thread worker function to gather report for each account 346 | def display_role(account, report, auth_spec): 347 | messages = [] 348 | overbar = '_' * (16 + len(account['Name'])) 349 | messages.append('\n%s' % overbar) 350 | messages.append("Account:\t%s" % account['Name']) 351 | credentials = get_assume_role_credentials( 352 | account['Id'], 353 | args['--org-access-role']) 354 | if isinstance(credentials, RuntimeError): 355 | messages.append(credentials) 356 | else: 357 | iam_client = boto3.client('iam', **credentials) 358 | iam_resource = boto3.resource('iam', **credentials) 359 | roles = [r for r in iam_client.list_roles()['Roles']] 360 | custom_policies = iam_client.list_policies(Scope='Local')['Policies'] 361 | if custom_policies: 362 | messages.append("Custom Policies:") 363 | for policy in custom_policies: 364 | messages.append(" %s" % policy['Arn']) 365 | messages.append("Roles:") 366 | for r in roles: 367 | role = iam_resource.Role(r['RoleName']) 368 | if not args['--full']: 369 | messages.append(" %s" % role.arn) 370 | else: 371 | principal = role.assume_role_policy_document['Statement'][0]['Principal'] 372 | if 'AWS' in principal: 373 | messages.append(" %s" % role.name) 374 | messages.append(" Arn:\t%s" % role.arn) 375 | messages.append(" Principal:\t%s" % principal['AWS']) 376 | attached = [p.policy_name for p 377 | in list(role.attached_policies.all())] 378 | if attached: 379 | messages.append(" Attached Policies:") 380 | for policy in attached: 381 | messages.append(" %s" % policy) 382 | report[account['Name']] = messages 383 | 384 | # gather report data from accounts 385 | report = {} 386 | queue_threads(log, deployed['accounts'], display_role, f_args=(report, auth_spec), 387 | thread_count=10) 388 | # process the reports 389 | header = "Provisioned IAM Roles in all Org Accounts:" 390 | overbar = '_' * len(header) 391 | log.info("\n\n%s\n%s" % (overbar, header)) 392 | for account, messages in sorted(report.items()): 393 | for msg in messages: 394 | log.info(msg) 395 | -------------------------------------------------------------------------------- /awsorgs/accounts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """Manage accounts in an AWS Organization. 5 | 6 | Usage: 7 | awsaccounts (report|create|update|invite) [--config FILE] 8 | [--spec-dir PATH] 9 | [--master-account-id ID] 10 | [--auth-account-id ID] 11 | [--org-access-role ROLE] 12 | [--invited-account-id ID] 13 | [--exec] [-q] [-d|-dd] 14 | awsaccounts (--help|--version) 15 | 16 | Modes of operation: 17 | report Display organization status report. 18 | create Create new accounts in AWS Org per specifation. 19 | update Set account alias and tags for each account per specifation. 20 | invite Invite another account to join Org as a member account. 21 | 22 | Options: 23 | -h, --help Show this help message and exit. 24 | -V, --version Display version info and exit. 25 | -f, --config FILE AWS Org config file in yaml format. 26 | --spec-dir PATH Location of AWS Org specification file directory. 27 | --master-account-id ID AWS account Id of the Org master account. 28 | --auth-account-id ID AWS account Id of the authentication account. 29 | --org-access-role ROLE IAM role for traversing accounts in the Org. 30 | --invited-account-id ID Id of account being invited to join Org. 31 | Required when running in 'invite' mode. 32 | --exec Execute proposed changes to AWS accounts. 33 | --role ROLENAME IAM role to use to access accounts. 34 | -q, --quiet Repress log output. 35 | -d, --debug Increase log level to 'DEBUG'. 36 | -dd Include botocore and boto3 logs in log stream. 37 | 38 | """ 39 | 40 | 41 | import yaml 42 | import time 43 | 44 | import boto3 45 | import botocore 46 | from botocore.exceptions import ClientError 47 | from docopt import docopt 48 | 49 | import awsorgs 50 | from awsorgs.utils import * 51 | from awsorgs.spec import * 52 | 53 | 54 | def create_accounts(org_client, args, log, deployed_accounts, account_spec): 55 | """ 56 | Compare deployed_accounts to list of accounts in the accounts spec. 57 | Create accounts not found in deployed_accounts. 58 | """ 59 | for a_spec in account_spec['accounts']: 60 | if not lookup(deployed_accounts, 'Name', a_spec['Name']): 61 | # check if it is still being provisioned 62 | created_accounts = scan_created_accounts(log, org_client) 63 | if lookup(created_accounts, 'AccountName', a_spec['Name']): 64 | log.warn("New account '%s' is not yet available" % a_spec['Name']) 65 | break 66 | # create a new account 67 | if 'Email' in a_spec and a_spec['Email']: 68 | email_addr = a_spec['Email'] 69 | else: 70 | email_addr = '%s@%s' % (a_spec['Name'], account_spec['default_domain']) 71 | log.info("Creating account '%s'" % (a_spec['Name'])) 72 | log.debug('account email: %s' % email_addr) 73 | if args['--exec']: 74 | new_account = org_client.create_account( 75 | AccountName=a_spec['Name'], 76 | Email=email_addr) 77 | create_id = new_account['CreateAccountStatus']['Id'] 78 | log.info("CreateAccountStatus Id: %s" % (create_id)) 79 | # validate creation status 80 | counter = 0 81 | maxtries = 5 82 | while counter < maxtries: 83 | creation = org_client.describe_create_account_status( 84 | CreateAccountRequestId=create_id 85 | )['CreateAccountStatus'] 86 | if creation['State'] == 'IN_PROGRESS': 87 | time.sleep(5) 88 | log.info("Account creation in progress for '%s'" % 89 | a_spec['Name']) 90 | elif creation['State'] == 'SUCCEEDED': 91 | log.info("Account creation succeeded") 92 | break 93 | elif creation['State'] == 'FAILED': 94 | log.error("Account creation failed: %s" % 95 | creation['FailureReason']) 96 | break 97 | counter += 1 98 | if counter == maxtries and creation['State'] == 'IN_PROGRESS': 99 | log.warn("Account creation still pending. Moving on!") 100 | 101 | 102 | def is_valid_account(account, account_spec): 103 | if account['Status'] != 'ACTIVE': 104 | return False 105 | a_spec = lookup(account_spec['accounts'], 'Name', account['Name']) 106 | if a_spec is None: 107 | return False 108 | return True 109 | 110 | 111 | def transform_tag_spec_into_list_of_dict(tag_spec): 112 | if tag_spec is not None: 113 | return [{'Key': k, 'Value': v} for k, v in tag_spec.items()] 114 | return [] 115 | 116 | 117 | def sorted_tags(tag_list): 118 | sorted_tag_key_names = sorted([tag['Key'] for tag in tag_list]) 119 | sorted_tags_ = [] 120 | for tag_key_name in sorted_tag_key_names: 121 | sorted_tags_ += [tag for tag in tag_list if tag['Key'] == tag_key_name] 122 | return sorted_tags_ 123 | 124 | 125 | def update_account_tags(org_client, account, account_tags, tag_spec): 126 | tagkeys = [tag['Key'] for tag in account_tags] 127 | org_client.untag_resource( 128 | ResourceId=account['Id'], 129 | TagKeys=tagkeys, 130 | ) 131 | org_client.tag_resource( 132 | ResourceId=account['Id'], 133 | Tags=tag_spec, 134 | ) 135 | 136 | 137 | def set_account_tags(account, log, args, account_spec, org_client): 138 | if not is_valid_account(account, account_spec): 139 | return 140 | tag_spec = lookup(account_spec['accounts'], 'Name', account['Name'], 'Tags') 141 | tag_spec = transform_tag_spec_into_list_of_dict(tag_spec) 142 | account_tags = org_client.list_tags_for_resource(ResourceId=account['Id'])['Tags'] 143 | log.debug('tag_spec for account "{}":\n{}'.format( 144 | account['Name'], 145 | yamlfmt(tag_spec), 146 | )) 147 | log.debug('account_tags for account "{}":\n{}'.format( 148 | account['Name'], 149 | yamlfmt(account_tags), 150 | )) 151 | if sorted_tags(account_tags) != sorted_tags(tag_spec): 152 | log.info('Updating tags for account "{}":\n{}'.format( 153 | account['Name'], 154 | string_differ(yamlfmt(account_tags), yamlfmt(tag_spec)), 155 | )) 156 | if args['--exec']: 157 | update_account_tags(org_client, account, account_tags, tag_spec) 158 | 159 | 160 | def set_account_alias(account, log, args, account_spec, role): 161 | """ 162 | Set an alias on an account. Use 'Alias' attribute from account spec 163 | if provided. Otherwise set the alias to the account name. 164 | """ 165 | if not is_valid_account(account, account_spec): 166 | return 167 | proposed_alias = lookup(account_spec['accounts'], 'Name', account['Name'], 'Alias') 168 | if proposed_alias is None: 169 | proposed_alias = account['Name'].lower() 170 | credentials = get_assume_role_credentials( 171 | account['Id'], args['--org-access-role']) 172 | if isinstance(credentials, RuntimeError): 173 | log.error(credentials) 174 | return 175 | else: 176 | iam_client = boto3.client('iam', **credentials) 177 | aliases = iam_client.list_account_aliases()['AccountAliases'] 178 | log.debug('account_name: %s; aliases: %s' % (account['Name'], aliases)) 179 | if not aliases: 180 | log.info("setting account alias to '%s' for account '%s'" % 181 | (proposed_alias, account['Name'])) 182 | if args['--exec']: 183 | try: 184 | iam_client.create_account_alias(AccountAlias=proposed_alias) 185 | except Exception as e: 186 | log.error(e) 187 | elif aliases[0] != proposed_alias: 188 | log.info("resetting account alias for account '%s' to '%s'; " 189 | "previous alias was '%s'" % 190 | (account['Name'], proposed_alias, aliases[0])) 191 | if args['--exec']: 192 | iam_client.delete_account_alias(AccountAlias=aliases[0]) 193 | try: 194 | iam_client.create_account_alias(AccountAlias=proposed_alias) 195 | except Exception as e: 196 | log.error(e) 197 | 198 | 199 | def scan_invited_accounts(log, org_client): 200 | """Return a list of handshake IDs""" 201 | response = org_client.list_handshakes_for_organization( 202 | Filter={'ActionType': 'INVITE'}) 203 | handshakes = response['Handshakes'] 204 | while 'NextToken' in response: 205 | response = org_client.list_handshakes_for_organization( 206 | Filter={'ActionType': 'INVITE'}, 207 | NextToken=response['NextToken']) 208 | handshakes += response['Handshakes'] 209 | log.debug(handshakes) 210 | return handshakes 211 | 212 | 213 | def invite_account(log, args, org_client, deployed_accounts): 214 | """Invite account_id to join Org""" 215 | account_id = args['--invited-account-id'] 216 | if not account_id: 217 | log.critical("option '--invited-account-id' not defined") 218 | sys.exit(1) 219 | if not valid_account_id(log, account_id): 220 | log.critical("option '--invited-account-id' must be a valid account Id") 221 | sys.exit(1) 222 | if lookup(deployed_accounts, 'Id', account_id): 223 | log.error("account %s already in organization" % account_id) 224 | return 225 | invited_accounts = scan_invited_accounts(log, org_client) 226 | account_invite = [invite for invite in invited_accounts 227 | if lookup(invite['Parties'], 'Type', 'ACCOUNT', 'Id') == account_id] 228 | if account_invite: 229 | log.debug('account_invite: %s' % account_invite) 230 | invite_state = account_invite[0]['State'] 231 | log.debug('invite_state: %s' % invite_state) 232 | if invite_state == 'ACCEPTED': 233 | log.error('Account %s has already accepted a previous invite' % account_id) 234 | return 235 | if invite_state in ['REQUESTED', 'OPEN']: 236 | log.error('Account %s has already been invited to Org and status is %s' % ( 237 | account_id, invite_state)) 238 | return 239 | log.info("inviting account %s to join Org" % account_id) 240 | if args['--exec']: 241 | target = dict(Id=account_id , Type='ACCOUNT') 242 | handshake = org_client.invite_account_to_organization(Target=target)['Handshake'] 243 | log.info('account invite handshake Id: %s' % handshake['Id']) 244 | return handshake 245 | return 246 | 247 | 248 | def display_invited_accounts(log, org_client): 249 | invited_accounts = scan_invited_accounts(log, org_client) 250 | if invited_accounts: 251 | header = "Invited Accounts in Org:" 252 | overbar = '_' * len(header) 253 | log.info("\n%s\n%s\n" % (overbar, header)) 254 | fmt_str = "{:16}{:12}{}" 255 | log.info(fmt_str.format('Id:', 'State:', 'Expires:')) 256 | for invite in invited_accounts: 257 | account_id = lookup(invite['Parties'], 'Type', 'ACCOUNT', 'Id') 258 | invite_state = invite['State'] 259 | invite_expiration = invite['ExpirationTimestamp'] 260 | log.info(fmt_str.format(account_id, invite_state, invite_expiration)) 261 | 262 | 263 | def display_provisioned_accounts(log, deployed_accounts, status): 264 | """ 265 | Print report of currently deployed accounts in AWS Organization. 266 | status:: matches account status (ACTIVE|SUSPENDED) 267 | """ 268 | if status not in ('ACTIVE', 'SUSPENDED'): 269 | raise RuntimeError("'status' must be one of ('ACTIVE', 'SUSPENDED')") 270 | sorted_account_names = sorted([a['Name'] for a in deployed_accounts 271 | if a['Status'] == status]) 272 | if sorted_account_names: 273 | header = '%s Accounts in Org:' % status.capitalize() 274 | overbar = '_' * len(header) 275 | log.info("\n%s\n%s\n" % (overbar, header)) 276 | fmt_str = "{:20}{:20}{:16}{}" 277 | log.info(fmt_str.format('Name:', 'Alias', 'Id:', 'Email:')) 278 | for name in sorted_account_names: 279 | account = lookup(deployed_accounts, 'Name', name) 280 | log.info(fmt_str.format( 281 | name, 282 | account['Alias'], 283 | account['Id'], 284 | account['Email'])) 285 | 286 | 287 | def unmanaged_accounts(log, deployed_accounts, account_spec): 288 | deployed_account_names = [a['Name'] for a in deployed_accounts] 289 | spec_account_names = [a['Name'] for a in account_spec['accounts']] 290 | log.debug('deployed_account_names: %s' % deployed_account_names) 291 | log.debug('spec_account_names: %s' % spec_account_names) 292 | return [a for a in deployed_account_names if a not in spec_account_names] 293 | 294 | 295 | def s3_object_for_accounts(s3_account_bucket, object_key, deployed_accounts): 296 | """ 297 | Post the deployed_accounts list to s3 bucket 298 | """ 299 | s3_client = boto3.client('s3') 300 | list_buckets_response = s3_client.list_buckets() 301 | bucket_list = [a['Name'] for a in list_buckets_response['Buckets']] 302 | if s3_account_bucket not in bucket_list: 303 | s3_client.create_bucket( 304 | ACL = 'private', 305 | Bucket = s3_account_bucket, 306 | CreateBucketConfiguration = {'LocationConstraint':'us-west-2'}) 307 | s3_client.put_object( 308 | Bucket = s3_account_bucket, 309 | Key = object_key, 310 | Body = yamlfmt(deployed_accounts)) 311 | return 312 | 313 | 314 | def main(): 315 | args = docopt(__doc__, version=awsorgs.__version__) 316 | log = get_logger(args) 317 | log.debug(args) 318 | args = load_config(log, args) 319 | credentials = get_assume_role_credentials( 320 | args['--master-account-id'], 321 | args['--org-access-role']) 322 | if isinstance(credentials, RuntimeError): 323 | log.critical(credentials) 324 | sys.exit(1) 325 | org_client = boto3.client('organizations', **credentials) 326 | root_id = get_root_id(org_client) 327 | deployed_accounts = scan_deployed_accounts(log, org_client) 328 | 329 | if args['report']: 330 | aliases = get_account_aliases(log, deployed_accounts, args['--org-access-role']) 331 | deployed_accounts = merge_aliases(log, deployed_accounts, aliases) 332 | display_provisioned_accounts(log, deployed_accounts, 'ACTIVE') 333 | display_provisioned_accounts(log, deployed_accounts, 'SUSPENDED') 334 | display_invited_accounts(log, org_client) 335 | s3_bucket = get_s3_bucket_name() 336 | s3_object_for_accounts(s3_bucket, S3_OBJECT_KEY, deployed_accounts) 337 | return 338 | 339 | account_spec = validate_spec(log, args) 340 | validate_master_id(org_client, account_spec) 341 | 342 | if args['create']: 343 | create_accounts(org_client, args, log, deployed_accounts, account_spec) 344 | unmanaged = unmanaged_accounts(log, deployed_accounts, account_spec) 345 | if unmanaged: 346 | log.warn("Unmanaged accounts in Org: %s" % (', '.join(unmanaged))) 347 | 348 | if args['update']: 349 | queue_threads(log, deployed_accounts, set_account_alias, 350 | f_args=(log, args, account_spec, args['--org-access-role']), 351 | thread_count=10) 352 | queue_threads(log, deployed_accounts, set_account_tags, 353 | f_args=(log, args, account_spec, org_client), 354 | thread_count=6) 355 | 356 | if args['invite']: 357 | invite_account(log, args, org_client, deployed_accounts) 358 | 359 | 360 | if __name__ == "__main__": 361 | main() 362 | -------------------------------------------------------------------------------- /docs/source/testing/test_awsauth.rst: -------------------------------------------------------------------------------- 1 | Functional Tests for awsauth tool 2 | ================================= 3 | 4 | Prerequisites: 5 | 6 | - admin access to auth account 7 | - spec file setup 8 | 9 | - install template spec files 10 | - spec files under git 11 | - site specific paramaters defined in common.yaml 12 | - configure ~.awsorgs/config.yaml 13 | - create at least one satelite account (see awsaccounts) 14 | 15 | 16 | 17 | Users and Groups - ``awsauth users`` 18 | ------------------------------------ 19 | 20 | Commands used: 21 | 22 | - git diff 23 | - awsauth users 24 | - awsauth users --exec 25 | - awsauth report --users 26 | 27 | 28 | Spec files impacted: 29 | 30 | - users-spec.yml 31 | - groups-spec.yml 32 | - custom-policy-spec.yml 33 | 34 | 35 | Actions Summary: 36 | 37 | - report IAM users and groups in accounts 38 | - create an IAM user and group, and add the user to the group 39 | - attach a IAM managed policy to your group 40 | - attach a IAM custom policy to your group 41 | - modify attached custom policy 42 | - detach policies, users from group 43 | - delete group, delete user 44 | 45 | 46 | 47 | Report users and groups in all accounts 48 | *************************************** 49 | 50 | Run ``awsauth report`` command with ``--users`` flag:: 51 | 52 | $ awsauth report --users 53 | _________________________________________ 54 | IAM Users and Groups in all Org Accounts: 55 | _____________________ 56 | Account: Managment 57 | Users: 58 | - arn:aws:iam::123456789011:user/awsauth/sysadm/agould 59 | - arn:aws:iam::123456789011:user/awsauth/drivera 60 | - arn:aws:iam::123456789011:user/awsauth/jhsu 61 | 62 | Groups: 63 | - arn:aws:iam::123456789011:group/awsauth/all-users 64 | - arn:aws:iam::123456789011:group/awsauth/orgadmins 65 | 66 | ____________________ 67 | Account: blee-dev 68 | ____________________ 69 | Account: blee-poc 70 | _____________________ 71 | Account: blee-prod 72 | __________________ 73 | Account: master 74 | Users: 75 | - arn:aws:iam::222222222222:user/agould 76 | 77 | Groups: 78 | - arn:aws:iam::222222222222:group/Admins 79 | 80 | 81 | Some variations:: 82 | 83 | $ awsauth report --users --full --account Managment 84 | $ awsauth report --users --full 85 | $ awsauth report --credentials --account Managment 86 | $ awsauth report --credentials 87 | 88 | 89 | 90 | Create an IAM user and group, and add the user to the group 91 | *********************************************************** 92 | 93 | Edit the following files: 94 | 95 | - users-spec.yml 96 | - groups-spec.yml 97 | 98 | Example Diff:: 99 | 100 | ~/.awsorgs/spec.d> git diff 101 | diff --git a/groups-spec.yml b/groups-spec.yml 102 | index 7f37144..d3fe879 100644 103 | --- a/groups-spec.yml 104 | +++ b/groups-spec.yml 105 | @@ -46,3 +46,8 @@ groups: 106 | 107 | + - Name: testers 108 | + Ensure: present 109 | + Members: 110 | + - joeuser 111 | + - maryuser 112 | diff --git a/users-spec.yml b/users-spec.yml 113 | index 22d2d61..5424bf4 100644 114 | --- a/users-spec.yml 115 | +++ b/users-spec.yml 116 | @@ -36,3 +36,6 @@ users: 117 | 118 | + - Name: joeuser 119 | + Email: joeuser@example.com 120 | + Team: test 121 | + - Name: maryuser 122 | + Email: maryuser@example.com 123 | + Team: test 124 | 125 | Review proposed changes in ``dry-run`` mode:: 126 | 127 | $ awsauth users 128 | 129 | Implement and review changes:: 130 | 131 | $ awsauth users --exec 132 | $ awsauth report --users 133 | 134 | 135 | Attach a IAM managed policy to your group 136 | ***************************************** 137 | 138 | Edit file ``groups-spec.yml`` 139 | 140 | Example Diff:: 141 | 142 | ~/.awsorgs/spec.d> git diff 143 | diff --git a/groups-spec.yml b/groups-spec.yml 144 | index d3fe879..9e05738 100644 145 | --- a/groups-spec.yml 146 | +++ b/groups-spec.yml 147 | @@ -50,4 +50,6 @@ groups: 148 | - Name: testers 149 | Ensure: present 150 | Members: 151 | - joeuser 152 | - maryuser 153 | + Policies: 154 | + - IAMReadOnlyAccess 155 | 156 | Review proposed changes in ``dry-run`` mode:: 157 | 158 | $ awsauth users 159 | 160 | Implement and review changes:: 161 | 162 | $ awsauth users --exec 163 | $ aws iam list-attached-group-policies --group-name testers 164 | 165 | 166 | Attach a IAM custom policy to your group 167 | **************************************** 168 | 169 | Edit the following files: 170 | 171 | - groups-spec.yml 172 | - custom-policy-spec.yml 173 | 174 | Example Diff:: 175 | 176 | ~/.awsorgs/spec.d> git diff 177 | diff --git a/custom-policy-spec.yml b/custom-policy-spec.yml 178 | index da46ebb..5d411f0 100644 179 | --- a/custom-policy-spec.yml 180 | +++ b/custom-policy-spec.yml 181 | @@ -111,3 +111,14 @@ custom_policies: 182 | Action: 183 | - aws-portal:Account* 184 | Resource: '*' 185 | + 186 | + - PolicyName: ReadS3Bucket 187 | + Description: list and get objects from my s3 bucket 188 | + Statement: 189 | + - Effect: Allow 190 | + Action: 191 | + - s3:List* 192 | + - s3:Get* 193 | + Resource: 194 | + - arn:aws:s3:::my_bucket 195 | + - arn:aws:s3:::my_bucket/* 196 | diff --git a/groups-spec.yml b/groups-spec.yml 197 | index b506856..11e87cb 100644 198 | --- a/groups-spec.yml 199 | +++ b/groups-spec.yml 200 | @@ -36,3 +36,4 @@ groups: 201 | - maryuser 202 | Policies: 203 | - IAMReadOnlyAccess 204 | + - ReadS3Bucket 205 | 206 | 207 | Review proposed changes in ``dry-run`` mode:: 208 | 209 | $ awsauth users 210 | 211 | Implement and review changes:: 212 | 213 | $ awsauth users --exec 214 | $ aws iam list-attached-group-policies --group-name testers 215 | $ aws iam get-policy --policy-arn 216 | 217 | 218 | Modify attached custom policy 219 | ***************************** 220 | 221 | Edit file ``custom-policy-spec.yml`` 222 | 223 | Example Diff:: 224 | 225 | ~/.awsorgs/spec.d> git diff 226 | diff --git a/custom-policy-spec.yml b/custom-policy-spec.yml 227 | index d6f29d7..7f5748a 100644 228 | --- a/custom-policy-spec.yml 229 | +++ b/custom-policy-spec.yml 230 | @@ -131,6 +131,8 @@ custom_policies: 231 | Resource: 232 | - arn:aws:s3:::my_bucket 233 | - arn:aws:s3:::my_bucket/* 234 | + - arn:aws:s3:::my_other_bucket 235 | + - arn:aws:s3:::my_other_bucket/* 236 | 237 | 238 | Review proposed changes in ``dry-run`` mode:: 239 | 240 | $ awsauth users 241 | 242 | Implement and review changes:: 243 | 244 | $ awsauth users --exec 245 | $ aws iam list-attached-group-policies --group-name testers 246 | $ aws iam get-policy --policy-arn 247 | $ aws iam get-policy-version --policy-arn --version-id 248 | 249 | 250 | Detach policies, users from group 251 | ********************************* 252 | 253 | Edit the following files: 254 | 255 | - groups-spec.yml 256 | 257 | Example Diff:: 258 | 259 | (python3.6) ashely@horus:~/.awsorgs/spec.d> git diff 260 | diff --git a/groups-spec.yml b/groups-spec.yml 261 | index 9e05738..565b1ab 100644 262 | --- a/groups-spec.yml 263 | +++ b/groups-spec.yml 264 | @@ -49,7 +49,4 @@ groups: 265 | - Name: testers 266 | Ensure: present 267 | Members: 268 | - - joeuser 269 | - - maryuser 270 | Policies: 271 | - - IAMReadOnlyAccess 272 | - - ReadS3Bucket 273 | 274 | 275 | Review proposed changes in ``dry-run`` mode:: 276 | 277 | $ awsauth users 278 | 279 | Implement and review changes:: 280 | 281 | $ awsauth users --exec 282 | $ awsauth report --users 283 | $ aws iam list-attached-group-policies --group-name testers 284 | $ aws iam get-policy --policy-arn 285 | 286 | 287 | Delete group, delete users 288 | ************************** 289 | 290 | Files to edit: 291 | 292 | - users-spec.yml 293 | - groups-spec.yml 294 | 295 | To delete IAM entities we must set attribute ``Ensure: absent`` to associated spec. 296 | 297 | Example diff:: 298 | 299 | (python3.6) ashely@horus:~/.awsorgs/spec.d> git diff 300 | diff --git a/groups-spec.yml b/groups-spec.yml 301 | index 9e05738..4eda72b 100644 302 | --- a/groups-spec.yml 303 | +++ b/groups-spec.yml 304 | @@ -47,9 +47,6 @@ groups: 305 | 306 | - Name: testers 307 | - Ensure: present 308 | + Ensure: absent 309 | Members: 310 | Policies: 311 | diff --git a/users-spec.yml b/users-spec.yml 312 | index 5424bf4..3e8b87d 100644 313 | --- a/users-spec.yml 314 | +++ b/users-spec.yml 315 | @@ -37,5 +37,6 @@ users: 316 | - Name: joeuser 317 | + Ensure: absent 318 | Email: joeuser@example.com 319 | Team: test 320 | - Name: maryuser 321 | + Ensure: absent 322 | Email: maryuser@example.com 323 | Team: test 324 | 325 | 326 | Review proposed changes in ``dry-run`` mode:: 327 | 328 | $ awsauth users 329 | 330 | Implement and review changes:: 331 | 332 | $ awsauth users --exec 333 | $ awsauth report --users 334 | 335 | 336 | 337 | Cross Account Access Delegations - ``awsauth delegations`` 338 | ---------------------------------------------------------- 339 | 340 | Prerequisites: 341 | 342 | - IAM group with users to use as ``TrustedGroup`` 343 | 344 | 345 | Commands used: 346 | 347 | - git diff 348 | - awsauth delegations 349 | - awsauth delegations --exec 350 | - awsauth report --roles 351 | 352 | 353 | Spec files impacted: 354 | 355 | - delegations-spec.yml 356 | - custom-policy-spec.yml 357 | 358 | 359 | Actions: 360 | 361 | - create a cross account access delegation 362 | - update the delegation definition 363 | - update attached custom policy 364 | - delete delegation 365 | 366 | 367 | Create a cross account access delegation 368 | **************************************** 369 | 370 | File to edit: delegations-spec.yml 371 | 372 | - set ``TrustedGroup`` to your new group 373 | - define a list of accounts in ``TrustingAccount`` 374 | - define one managed policy in ``Policies`` 375 | 376 | Example Diff:: 377 | 378 | ~/.awsorgs/spec.d> git diff 379 | diff --git a/delegations-spec.yml b/delegations-spec.yml 380 | index 1ae3245..4d571e9 100644 381 | --- a/delegations-spec.yml 382 | +++ b/delegations-spec.yml 383 | @@ -101,3 +101,14 @@ delegations: 384 | 385 | + - RoleName: TestersRole 386 | + Ensure: present 387 | + Description: testing cross account delegation 388 | + TrustingAccount: 389 | + TrustedGroup: testers 390 | + RequireMFA: True 391 | + Policies: 392 | + - ReadOnlyAccess 393 | 394 | 395 | Review proposed changes in ``dry-run`` mode:: 396 | 397 | $ awsauth delegations 398 | 399 | Implement and review changes:: 400 | 401 | $ awsauth delegations --exec 402 | $ awsauth report --roles | egrep "^Account|TestersRole" 403 | $ aws iam list-group-policies --group-name testers 404 | 405 | 406 | Update the delegation to apply to all accounts 407 | ********************************************** 408 | 409 | File to edit: delegations-spec.yml 410 | 411 | - set ``TrustingAccount`` to keyword ``ALL`` 412 | 413 | Example Diff:: 414 | 415 | ~/.awsorgs/spec.d> git diff 416 | diff --git a/delegations-spec.yml b/delegations-spec.yml 417 | index 282db35..e46ac9e 100644 418 | --- a/delegations-spec.yml 419 | +++ b/delegations-spec.yml 420 | @@ -104,14 +104,10 @@ delegations: 421 | - RoleName: TestersRole 422 | Ensure: present 423 | Description: testing cross account delegation 424 | - TrustingAccount: 425 | - - blee-dev 426 | - - blee-poc 427 | - - blee-prod 428 | + TrustingAccount: ALL 429 | TrustedGroup: testers 430 | RequireMFA: True 431 | Policies: 432 | - ReadOnlyAccess 433 | 434 | Review proposed changes in ``dry-run`` mode:: 435 | 436 | $ awsauth delegations 437 | 438 | Implement and review changes:: 439 | 440 | $ awsauth delegations --exec 441 | $ awsauth report --roles | egrep "^Account|TestersRole" 442 | $ aws iam list-group-policies --group-name testers 443 | $ aws iam get-group-policy --group-name testers --policy-name AllowAssumeRole-TestersRole 444 | 445 | 446 | Exclude some accounts from a delegation 447 | *************************************** 448 | 449 | File to edit: delegations-spec.yml 450 | 451 | - define a list of accounts in ``ExcludeAccounts`` 452 | 453 | Example Diff:: 454 | 455 | :~/.awsorgs/spec.d> git diff 456 | diff --git a/delegations-spec.yml b/delegations-spec.yml 457 | index e46ac9e..8b01bb8 100644 458 | --- a/delegations-spec.yml 459 | +++ b/delegations-spec.yml 460 | @@ -105,6 +105,10 @@ delegations: 461 | Ensure: present 462 | Description: testing cross account delegation 463 | TrustingAccount: ALL 464 | + ExcludeAccounts: 465 | + - blee-dev 466 | + - blee-prod 467 | TrustedGroup: testers 468 | RequireMFA: True 469 | 470 | 471 | Review proposed changes in ``dry-run`` mode:: 472 | 473 | $ awsauth delegations 474 | 475 | Implement and review changes:: 476 | 477 | $ awsauth delegations --exec 478 | $ awsauth report --roles | egrep "^Account|TestersRole" 479 | $ aws iam list-group-policies --group-name testers 480 | $ aws iam get-group-policy --group-name testers --policy-name AllowAssumeRole-TestersRole 481 | $ aws iam get-group-policy --group-name testers --policy-name DenyAssumeRole-TestersRole 482 | 483 | 484 | Attach a custom policy 485 | ********************** 486 | 487 | Files to edit: 488 | 489 | - custom-policy-spec.yml 490 | - delegations-spec.yml 491 | 492 | Example Diff:: 493 | 494 | ~/.awsorgs/spec.d> git diff 495 | diff --git a/custom-policy-spec.yml b/custom-policy-spec.yml 496 | index 9399a60..a428164 100644 497 | --- a/custom-policy-spec.yml 498 | +++ b/custom-policy-spec.yml 499 | @@ -120,3 +120,14 @@ custom_policies: 500 | + 501 | + - PolicyName: ReadS3Bucket 502 | + Description: list and get objects from my s3 bucket 503 | + Statement: 504 | + - Effect: Allow 505 | + Action: 506 | + - s3:List* 507 | + - s3:Get* 508 | + Resource: 509 | + - arn:aws:s3:::my_bucket 510 | + - arn:aws:s3:::my_bucket/* 511 | diff --git a/delegations-spec.yml b/delegations-spec.yml 512 | index 8b01bb8..ce9afa9 100644 513 | --- a/delegations-spec.yml 514 | +++ b/delegations-spec.yml 515 | @@ -113,5 +113,6 @@ delegations: 516 | RequireMFA: True 517 | Policies: 518 | - ReadOnlyAccess 519 | + - ReadS3Bucket 520 | 521 | 522 | Review proposed changes in ``dry-run`` mode:: 523 | 524 | $ awsauth delegations 525 | 526 | Implement and review changes:: 527 | 528 | $ awsauth delegations --exec 529 | $ awsauth report --roles | egrep "^Account|awsauth/ReadS3Bucket" 530 | $ aws iam list-group-policies --group-name testers 531 | $ aws iam get-group-policy --group-name testers --policy-name AllowAssumeRole-TestersRole 532 | $ aws iam get-group-policy --group-name testers --policy-name DenyAssumeRole-TestersRole 533 | 534 | 535 | Modify a custom policy 536 | ********************** 537 | 538 | Files to edit: 539 | 540 | - custom-policy-spec.yml 541 | 542 | Example Diff:: 543 | 544 | ~/.awsorgs/spec.d> git diff 545 | diff --git a/custom-policy-spec.yml b/custom-policy-spec.yml 546 | index a428164..7efe46b 100644 547 | --- a/custom-policy-spec.yml 548 | +++ b/custom-policy-spec.yml 549 | @@ -131,3 +131,5 @@ custom_policies: 550 | Resource: 551 | - arn:aws:s3:::my_bucket 552 | - arn:aws:s3:::my_bucket/* 553 | + - arn:aws:s3:::my_other_bucket 554 | + - arn:aws:s3:::my_other_bucket/* 555 | 556 | Review proposed changes in ``dry-run`` mode:: 557 | 558 | $ awsauth delegations 559 | 560 | Implement and review changes:: 561 | 562 | $ awsauth delegations --exec 563 | $ awsauth report --roles --full | grep -A12 awsauth/ReadS3Bucket 564 | 565 | 566 | 567 | Delete the delegation from all accounts 568 | *************************************** 569 | 570 | Files to edit: delegations-spec.yml 571 | 572 | - set ``Ensure: absent`` 573 | 574 | Example Diff:: 575 | 576 | ~/.awsorgs/spec.d> git diff 577 | diff --git a/delegations-spec.yml b/delegations-spec.yml 578 | index 2b050da..b6892d1 100644 579 | --- a/delegations-spec.yml 580 | +++ b/delegations-spec.yml 581 | @@ -67,14 +67,10 @@ delegations: 582 | - ViewBilling 583 | 584 | - RoleName: TestersRole 585 | - Ensure: present 586 | + Ensure: absent 587 | Description: testing cross account delegation 588 | TrustingAccount: ALL 589 | ExcludeAccounts: 590 | - blee-poc 591 | - blee-dev 592 | - blee-prod 593 | 594 | Review proposed changes in ``dry-run`` mode:: 595 | 596 | $ awsauth delegations 597 | 598 | Implement and review changes:: 599 | 600 | $ awsauth delegations --exec 601 | $ awsauth report --roles | egrep "^Account|role/awsauth/ReadS3Bucket" 602 | $ aws iam list-group-policies --group-name testers 603 | 604 | -------------------------------------------------------------------------------- /awsorgs/orgs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """Manage recources in an AWS Organization. 5 | 6 | Usage: 7 | awsorgs (report|organization) [--config FILE] 8 | [--spec-dir PATH] 9 | [--master-account-id ID] 10 | [--auth-account-id ID] 11 | [--org-access-role ROLE] 12 | [--exec] [-q] [-d|-dd] 13 | awsorgs (--help|--version) 14 | 15 | Modes of operation: 16 | report Display organization status report only. 17 | orgnanizaion Run AWS Org management tasks per specification. 18 | 19 | Options: 20 | -h, --help Show this help message and exit. 21 | -V, --version Display version info and exit. 22 | --config FILE AWS Org config file in yaml format. 23 | --spec-dir PATH Location of AWS Org specification file directory. 24 | --master-account-id ID AWS account Id of the Org master account. 25 | --auth-account-id ID AWS account Id of the authentication account. 26 | --org-access-role ROLE IAM role for traversing accounts in the Org. 27 | --exec Execute proposed changes to AWS Org. 28 | -q, --quiet Repress log output. 29 | -d, --debug Increase log level to 'DEBUG'. 30 | -dd Include botocore and boto3 logs in log stream. 31 | 32 | """ 33 | 34 | 35 | import yaml 36 | import json 37 | import time 38 | 39 | import boto3 40 | from docopt import docopt 41 | 42 | import awsorgs 43 | import awsorgs.utils 44 | from awsorgs.utils import * 45 | from awsorgs.spec import * 46 | 47 | 48 | def validate_accounts_unique_in_org(log, root_spec): 49 | """ 50 | Make sure accounts are unique across org 51 | """ 52 | # recursively build mapping of accounts to ou_names 53 | def map_accounts(spec, account_map={}): 54 | if 'Accounts' in spec and spec['Accounts']: 55 | for account in spec['Accounts']: 56 | if account in account_map: 57 | account_map[account].append(spec['Name']) 58 | else: 59 | account_map[account] = [(spec['Name'])] 60 | if 'Child_OU' in spec and spec['Child_OU']: 61 | for child_spec in spec['Child_OU']: 62 | map_accounts(child_spec, account_map) 63 | return account_map 64 | # find accounts set to more than one OU 65 | unique = True 66 | for account, ou in list(map_accounts(root_spec).items()): 67 | if len(ou) > 1: 68 | log.error("Account '%s' set in multiple OU: %s" % (account, ou)) 69 | unique = False 70 | if not unique: 71 | log.critical("Invalid org_spec: Do not assign accounts to multiple " 72 | "Organizatinal Units") 73 | sys.exit(1) 74 | 75 | 76 | def enable_policy_type_in_root(org_client, root_id): 77 | """ 78 | Ensure policy type 'SERVICE_CONTROL_POLICY' is enabled in the 79 | organization root. 80 | """ 81 | p_type = org_client.list_roots()['Roots'][0]['PolicyTypes'] 82 | if (not p_type or (p_type[0]['Type'] == 'SERVICE_CONTROL_POLICY' 83 | and p_type[0]['Status'] != 'ENABLED')): 84 | org_client.enable_policy_type(RootId=root_id, PolicyType='SERVICE_CONTROL_POLICY') 85 | 86 | 87 | def get_parent_id(org_client, account_id): 88 | """ 89 | Query deployed AWS organanization for 'account_id. Return the 'Id' of 90 | the parent OrganizationalUnit or 'None'. 91 | """ 92 | parents = org_client.list_parents(ChildId=account_id)['Parents'] 93 | try: 94 | len(parents) == 1 95 | return parents[0]['Id'] 96 | except: 97 | raise RuntimeError("API Error: account '%s' has more than one parent: " 98 | % (account_id, parents)) 99 | 100 | 101 | def list_policies_in_ou (org_client, ou_id): 102 | """ 103 | Query deployed AWS organanization. Return a list (of type dict) 104 | of policies attached to OrganizationalUnit referenced by 'ou_id'. 105 | """ 106 | policies_in_ou = org_client.list_policies_for_target( 107 | TargetId=ou_id, Filter='SERVICE_CONTROL_POLICY',)['Policies'] 108 | return sorted([ou['Name'] for ou in policies_in_ou]) 109 | 110 | 111 | def scan_deployed_policies(org_client): 112 | """ 113 | Return list of Service Control Policies deployed in Organization 114 | """ 115 | return org_client.list_policies(Filter='SERVICE_CONTROL_POLICY')['Policies'] 116 | 117 | 118 | def scan_deployed_ou(log, org_client, root_id): 119 | """ 120 | Recursively traverse deployed AWS Organization. Return list of 121 | organizational unit dictionaries. 122 | """ 123 | def build_deployed_ou_table(org_client, parent_name, parent_id, deployed_ou): 124 | # recusive sub function to build the 'deployed_ou' table 125 | response = org_client.list_organizational_units_for_parent( ParentId=parent_id) 126 | child_ou = response['OrganizationalUnits'] 127 | while 'NextToken' in response and response['NextToken']: 128 | response = org_client.list_organizational_units_for_parent( 129 | ParentId=parent_id, NextToken=response['NextToken']) 130 | child_ou += response['OrganizationalUnits'] 131 | 132 | response = org_client.list_accounts_for_parent( ParentId=parent_id) 133 | accounts = response['Accounts'] 134 | while 'NextToken' in response and response['NextToken']: 135 | response = org_client.list_accounts_for_parent( 136 | ParentId=parent_id, NextToken=response['NextToken']) 137 | accounts += response['Accounts'] 138 | log.debug('parent_name: %s; ou: %s' % (parent_name, yamlfmt(child_ou))) 139 | log.debug('parent_name: %s; accounts: %s' % (parent_name, yamlfmt(accounts))) 140 | 141 | if not deployed_ou: 142 | deployed_ou.append(dict( 143 | Name = parent_name, 144 | Id = parent_id, 145 | Child_OU = [ou['Name'] for ou in child_ou if 'Name' in ou], 146 | Accounts = [acc['Name'] for acc in accounts if 'Name' in acc])) 147 | else: 148 | for ou in deployed_ou: 149 | if ou['Name'] == parent_name: 150 | ou['Child_OU'] = [d['Name'] for d in child_ou] 151 | ou['Accounts'] = [d['Name'] for d in accounts] 152 | for ou in child_ou: 153 | ou['ParentId'] = parent_id 154 | deployed_ou.append(ou) 155 | build_deployed_ou_table(org_client, ou['Name'], ou['Id'], deployed_ou) 156 | 157 | # build the table 158 | deployed_ou = [] 159 | build_deployed_ou_table(org_client, 'root', root_id, deployed_ou) 160 | log.debug(yamlfmt(deployed_ou)) 161 | return deployed_ou 162 | 163 | 164 | def display_provisioned_policies(org_client, log, deployed): 165 | """ 166 | Print report of currently deployed Service Control Policies in 167 | AWS Organization. 168 | """ 169 | header = "Provisioned Service Control Policies:" 170 | overbar = '_' * len(header) 171 | log.info("\n\n%s\n%s" % (overbar, header)) 172 | for policy in deployed['policies']: 173 | log.info("\nName:\t\t%s" % policy['Name']) 174 | log.info("Description:\t%s" % policy['Description']) 175 | log.info("Id:\t%s" % policy['Id']) 176 | log.info("Content:") 177 | log.info(json.dumps(json.loads(org_client.describe_policy( 178 | PolicyId=policy['Id'])['Policy']['Content']), 179 | indent=2, 180 | separators=(',', ': '))) 181 | 182 | 183 | def display_provisioned_ou(org_client, log, deployed_ou, parent_name, indent=0): 184 | """ 185 | Recursive function to display the deployed AWS Organization structure. 186 | """ 187 | # query aws for child orgs 188 | parent_id = lookup(deployed_ou, 'Name', parent_name, 'Id') 189 | child_ou_list = lookup(deployed_ou, 'Name', parent_name, 'Child_OU') 190 | child_accounts = lookup(deployed_ou, 'Name', parent_name, 'Accounts') 191 | # display parent ou name 192 | tab = ' ' 193 | log.info(tab*indent + parent_name + ':') 194 | # look for policies 195 | policy_names = list_policies_in_ou(org_client, parent_id) 196 | if len(policy_names) > 0: 197 | log.info(tab*indent + tab + 'Policies: ' + ', '.join(policy_names)) 198 | # look for accounts 199 | account_list = sorted(child_accounts) 200 | if len(account_list) > 0: 201 | log.info(tab*indent + tab + 'Accounts: ' + ', '.join(account_list)) 202 | # look for child OUs 203 | if child_ou_list: 204 | log.info(tab*indent + tab + 'Child_OU:') 205 | indent+=2 206 | for ou_name in child_ou_list: 207 | # recurse 208 | display_provisioned_ou(org_client, log, deployed_ou, ou_name, indent) 209 | 210 | 211 | def manage_account_moves(org_client, args, log, deployed, ou_spec, dest_parent_id): 212 | """ 213 | Alter deployed AWS Organization. Ensure accounts are contained 214 | by designated OrganizationalUnits based on OU specification. 215 | """ 216 | if 'Accounts' in ou_spec and ou_spec['Accounts']: 217 | for account in ou_spec['Accounts']: 218 | account_id = lookup(deployed['accounts'], 'Name', account, 'Id') 219 | if not account_id: 220 | log.warn("Account '%s' not yet in Organization" % account) 221 | else: 222 | source_parent_id = get_parent_id(org_client, account_id) 223 | if dest_parent_id != source_parent_id: 224 | log.info("Moving account '%s' to OU '%s'" % 225 | (account, ou_spec['Name'])) 226 | if args['--exec']: 227 | org_client.move_account( 228 | AccountId=account_id, 229 | SourceParentId=source_parent_id, 230 | DestinationParentId=dest_parent_id) 231 | 232 | 233 | def place_unmanged_accounts(org_client, args, log, deployed, account_list, dest_parent): 234 | """ 235 | Move any unmanaged accounts into the default OU. 236 | """ 237 | for account in account_list: 238 | account_id = lookup(deployed['accounts'], 'Name', account, 'Id') 239 | dest_parent_id = lookup(deployed['ou'], 'Name', dest_parent, 'Id') 240 | source_parent_id = get_parent_id(org_client, account_id) 241 | if dest_parent_id and dest_parent_id != source_parent_id: 242 | log.info("Moving unmanged account '%s' to default OU '%s'" % 243 | (account, dest_parent)) 244 | if args['--exec']: 245 | org_client.move_account( 246 | AccountId=account_id, 247 | SourceParentId=source_parent_id, 248 | DestinationParentId=dest_parent_id) 249 | 250 | 251 | def manage_policies(org_client, args, log, deployed, org_spec): 252 | """ 253 | Manage Service Control Policies in the AWS Organization. Make updates 254 | according to the sc_policies specification. Do not touch 255 | the default policy. Do not delete an attached policy. 256 | """ 257 | for p_spec in org_spec['sc_policies']: 258 | policy_name = p_spec['PolicyName'] 259 | log.debug("considering sc_policy: %s" % policy_name) 260 | # dont touch default policy 261 | if policy_name == org_spec['default_sc_policy']: 262 | continue 263 | policy = lookup(deployed['policies'], 'Name', policy_name) 264 | # delete existing sc_policy 265 | if ensure_absent(p_spec): 266 | if policy: 267 | log.info("Deleting policy '%s'" % (policy_name)) 268 | # dont delete attached policy 269 | if org_client.list_targets_for_policy(PolicyId=policy['Id'])['Targets']: 270 | log.error("Cannot delete policy '%s'. Still attached to OU" % 271 | policy_name) 272 | elif args['--exec']: 273 | org_client.delete_policy(PolicyId=policy['Id']) 274 | continue 275 | # create or update sc_policy 276 | policy_doc = json.dumps(dict(Version='2012-10-17', Statement=p_spec['Statement'])) 277 | log.debug("spec sc_policy_doc: %s" % yamlfmt(policy_doc)) 278 | # create new policy 279 | if not policy: 280 | log.info("Creating policy '%s'" % policy_name) 281 | if args['--exec']: 282 | org_client.create_policy( 283 | Content=policy_doc, 284 | Description=p_spec['Description'], 285 | Name=p_spec['PolicyName'], 286 | Type='SERVICE_CONTROL_POLICY') 287 | # check for policy updates 288 | else: 289 | deployed_policy_doc = json.dumps(json.loads(org_client.describe_policy( 290 | PolicyId=policy['Id'])['Policy']['Content'])) 291 | log.debug("real sc_policy_doc: %s" % yamlfmt(deployed_policy_doc)) 292 | if (p_spec['Description'] != policy['Description'] 293 | or policy_doc != deployed_policy_doc): 294 | log.info("Updating policy '%s'" % policy_name) 295 | if args['--exec']: 296 | org_client.update_policy( 297 | PolicyId=policy['Id'], 298 | Content=policy_doc, 299 | Description=p_spec['Description'],) 300 | 301 | 302 | def manage_policy_attachments(org_client, args, log, deployed, org_spec, ou_spec, ou_id): 303 | """ 304 | Attach or detach specified Service Control Policy to a deployed 305 | OrganizatinalUnit. Do not detach the default policy ever. 306 | """ 307 | # create lists policies_to_attach and policies_to_detach 308 | attached_policy_list = list_policies_in_ou(org_client, ou_id) 309 | if 'SC_Policies' in ou_spec and isinstance(ou_spec['SC_Policies'], list): 310 | spec_policy_list = ou_spec['SC_Policies'] 311 | else: 312 | spec_policy_list = [] 313 | policies_to_attach = [p for p in spec_policy_list 314 | if p not in attached_policy_list] 315 | policies_to_detach = [p for p in attached_policy_list 316 | if p not in spec_policy_list 317 | and p != org_spec['default_sc_policy']] 318 | # attach policies 319 | for policy_name in policies_to_attach: 320 | if not lookup(deployed['policies'],'Name',policy_name): 321 | if args['--exec']: 322 | raise RuntimeError("spec-file: ou_spec: policy '%s' not defined" % 323 | policy_name) 324 | if not ensure_absent(ou_spec): 325 | log.info("Attaching policy '%s' to OU '%s'" % (policy_name, ou_spec['Name'])) 326 | if args['--exec']: 327 | org_client.attach_policy( 328 | PolicyId=lookup(deployed['policies'], 'Name', policy_name, 'Id'), 329 | TargetId=ou_id) 330 | # detach policies 331 | for policy_name in policies_to_detach: 332 | log.info("Detaching policy '%s' from OU '%s'" % (policy_name, ou_spec['Name'])) 333 | if args['--exec']: 334 | org_client.detach_policy( 335 | PolicyId=lookup(deployed['policies'], 'Name', policy_name, 'Id'), 336 | TargetId=ou_id) 337 | 338 | 339 | def manage_ou(org_client, args, log, deployed, org_spec, ou_spec_list, parent_name): 340 | """ 341 | Recursive function to manage OrganizationalUnits in the AWS 342 | Organization. 343 | """ 344 | for ou_spec in ou_spec_list: 345 | # ou exists 346 | ou = lookup(deployed['ou'], 'Name', ou_spec['Name']) 347 | if ou: 348 | # check for child_ou. recurse before other tasks. 349 | if 'Child_OU' in ou_spec: 350 | manage_ou(org_client, args, log, deployed, org_spec, 351 | ou_spec['Child_OU'], ou_spec['Name']) 352 | # check if ou 'absent' 353 | if ensure_absent(ou_spec): 354 | log.info("Deleting OU %s" % ou_spec['Name']) 355 | # error if ou contains anything 356 | error_flag = False 357 | for key in ['Accounts', 'SC_Policies', 'Child_OU']: 358 | if key in ou and ou[key]: 359 | log.error("Can not delete OU '%s'. deployed '%s' exists." % 360 | (ou_spec['Name'], key)) 361 | error_flag = True 362 | if error_flag: 363 | continue 364 | elif args['--exec']: 365 | org_client.delete_organizational_unit( 366 | OrganizationalUnitId=ou['Id']) 367 | # manage account and sc_policy placement in OU 368 | else: 369 | manage_policy_attachments(org_client, args, log, 370 | deployed, org_spec, ou_spec, ou['Id']) 371 | manage_account_moves(org_client, args, log, deployed, ou_spec, ou['Id']) 372 | # create new OU 373 | elif not ensure_absent(ou_spec): 374 | log.info("Creating new OU '%s' under parent '%s'" % 375 | (ou_spec['Name'], parent_name)) 376 | if args['--exec']: 377 | new_ou = org_client.create_organizational_unit( 378 | ParentId=lookup(deployed['ou'],'Name',parent_name,'Id'), 379 | Name=ou_spec['Name'])['OrganizationalUnit'] 380 | # account and sc_policy placement 381 | manage_policy_attachments(org_client, args, log, 382 | deployed, org_spec, ou_spec, new_ou['Id']) 383 | manage_account_moves(org_client, args, log, deployed, ou_spec, new_ou['Id']) 384 | # recurse if child OU 385 | if ('Child_OU' in ou_spec and isinstance(new_ou, dict) 386 | and 'Id' in new_ou): 387 | manage_ou(org_client, args, log, deployed, org_spec, 388 | ou_spec['Child_OU'], new_ou['Name']) 389 | 390 | 391 | def main(): 392 | args = docopt(__doc__, version=awsorgs.__version__) 393 | log = get_logger(args) 394 | log.debug(args) 395 | args = load_config(log, args) 396 | credentials = get_assume_role_credentials( 397 | args['--master-account-id'], 398 | args['--org-access-role']) 399 | if isinstance(credentials, RuntimeError): 400 | log.critical(credentials) 401 | sys.exit(1) 402 | org_client = boto3.client('organizations', **credentials) 403 | root_id = get_root_id(org_client) 404 | deployed = dict( 405 | policies = scan_deployed_policies(org_client), 406 | accounts = scan_deployed_accounts(log, org_client), 407 | ou = scan_deployed_ou(log, org_client, root_id)) 408 | 409 | if args['report']: 410 | header = 'Provisioned Organizational Units in Org:' 411 | overbar = '_' * len(header) 412 | log.info("\n%s\n%s" % (overbar, header)) 413 | display_provisioned_ou(org_client, log, deployed['ou'], 'root') 414 | display_provisioned_policies(org_client, log, deployed) 415 | 416 | if args['organization']: 417 | org_spec = validate_spec(log, args) 418 | root_spec = lookup(org_spec['organizational_units'], 'Name', 'root') 419 | validate_master_id(org_client, org_spec) 420 | validate_accounts_unique_in_org(log, root_spec) 421 | managed = dict( 422 | accounts = search_spec(root_spec, 'Accounts', 'Child_OU'), 423 | ou = search_spec(root_spec, 'Name', 'Child_OU'), 424 | policies = [p['PolicyName'] for p in org_spec['sc_policies']]) 425 | 426 | # ensure default_sc_policy is considered 'managed' 427 | if org_spec['default_sc_policy'] not in managed['policies']: 428 | managed['policies'].append(org_spec['default_sc_policy']) 429 | enable_policy_type_in_root(org_client, root_id) 430 | manage_policies(org_client, args, log, deployed, org_spec) 431 | 432 | # rescan deployed policies 433 | deployed['policies'] = scan_deployed_policies(org_client) 434 | manage_ou(org_client, args, log, deployed, org_spec, 435 | org_spec['organizational_units'], 'root') 436 | 437 | # check for unmanaged resources 438 | for key in list(managed.keys()): 439 | unmanaged= [a['Name'] for a in deployed[key] if a['Name'] not in managed[key]] 440 | if unmanaged: 441 | log.warn("Unmanaged %s in Organization: %s" % (key,', '.join(unmanaged))) 442 | if key == 'accounts': 443 | # append unmanaged accounts to default_ou 444 | place_unmanged_accounts(org_client, args, log, deployed, 445 | unmanaged, org_spec['default_ou']) 446 | 447 | 448 | if __name__ == "__main__": 449 | main() 450 | --------------------------------------------------------------------------------