├── media ├── .gitkeep ├── cloudshell.png ├── cloudshell_step1.png ├── cloudshell_step3.png ├── cloudshell_list_step1.png ├── cloudshell_list_step2.png ├── cloudshell_delete_step1.png └── cloudshell_update_step1.png ├── deploy.sh ├── CODE_OF_CONDUCT.md ├── LICENSE ├── CONTRIBUTING.md ├── README.md └── script.py /media/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/cloudshell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/HEAD/media/cloudshell.png -------------------------------------------------------------------------------- /media/cloudshell_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/HEAD/media/cloudshell_step1.png -------------------------------------------------------------------------------- /media/cloudshell_step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/HEAD/media/cloudshell_step3.png -------------------------------------------------------------------------------- /media/cloudshell_list_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/HEAD/media/cloudshell_list_step1.png -------------------------------------------------------------------------------- /media/cloudshell_list_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/HEAD/media/cloudshell_list_step2.png -------------------------------------------------------------------------------- /media/cloudshell_delete_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/HEAD/media/cloudshell_delete_step1.png -------------------------------------------------------------------------------- /media/cloudshell_update_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/HEAD/media/cloudshell_update_step1.png -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | python3 -m pip install simple-term-menu 5 | wget https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/main/script.py 6 | python3 script.py -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Organizations Alternate Contact Manager 2 | 3 | ### Programmatically manage alternate contacts in member accounts of an AWS Organizations 4 | 5 | In this repository, we share code for batch management of alternate contacts from your AWS accounts in an [AWS Organizations](https://aws.amazon.com/organizations/). [Here](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-account-payment.html#manage-account-payment-alternate-contacts) are some use cases and the importance of always keeping your Alternate Contacts data up to date. 6 | 7 | Reference: [Programmatically managing alternate contacts on member accounts with AWS Organizations](https://aws.amazon.com/blogs/mt/programmatically-managing-alternate-contacts-on-member-accounts-with-aws-organizations/). 8 | 9 | ### [Requirements](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html#update-alternate-contact-requirement) 10 | 11 | - Your organization must enable all features to manage settings on your member accounts. This allows admin control over the member accounts. This is set by default when you create your organization. If your organization is set to consolidated billing only, and you want to enable all features,” see [Enabling all features in your organization](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_org_support-all-features.html). 12 | - You need to enable trusted access for AWS Account Management service. To set this up, see [Enabling trusted access for AWS Account Management](https://docs.aws.amazon.com/accounts/latest/reference/using-orgs-trusted-access.html). 13 | 14 | ### How to run 15 | 16 | 1. Open CloudShell 17 | 18 | ![img](media/cloudshell.png) 19 | 20 | 2. When CloudShell opens, we will run the following command: 21 | 22 | 1. For the first time, we will run the following command to download all dependencies: 23 | 24 | wget https://raw.githubusercontent.com/aws-samples/aws-organizations-alternate-contact-manager/main/deploy.sh 25 | chmod +x deploy.sh 26 | ./deploy.sh 27 | 28 | 2. For the next times we can just run: 29 | 30 | python3 script.py 31 | 32 | 3. After running the above commands in CloudShell, the first step is to choose one of the 3 action options. 33 | 34 | ![img](media/cloudshell_step1.png) 35 | 36 | 4. Second step is to input a list of account ids separated by comma or all. For the Delete action, for security reasons, it is only possible to run one account id at a time. Below are some input examples: 37 | 38 | - all 39 | - 000000000000,111111111111,222222222222,333333333333 40 | - 000000000000, 111111111111, 222222222222, 333333333333 41 | - 012345678910 _(valid for Delete action)_ 42 | 43 | 5. The third step is to choose which type of alternate contact. 44 | 45 | ![img](media/cloudshell_step3.png) 46 | 47 | #### 6. List 48 | 49 | 1. For List action, there is the option to export the result to an s3 bucket. 50 | 51 | ![img](media/cloudshell_list_step1.png) 52 | 53 | 2. Inputting "y" will ask for the name of an S3 bucket to upload. Inputting "n", the result will return on the CloudShell screen. 54 | 55 | ![img](media/cloudshell_list_step2.png) 56 | 57 | #### 7. Update 58 | 59 | 1. For Update action, it will be required to fill in all the contact fields, if you must pay attention to the correct pattern. Below are some input examples: 60 | - Email: example@mail.com 61 | - Name: My Name 62 | - Phone number: +5511900002222 63 | - Title: Technical Account Manager 64 | 65 | ![img](media/cloudshell_update_step1.png) 66 | 67 | #### 8. Delete 68 | 69 | 1. For the Delete action, for security reasons, it is only possible to run one account id at a time. 70 | 71 | ![img](media/cloudshell_delete_step1.png) 72 | -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import boto3 5 | import json 6 | import logging 7 | import time 8 | from simple_term_menu import TerminalMenu 9 | from botocore.exceptions import ClientError 10 | from datetime import datetime 11 | from pprint import pprint 12 | 13 | 14 | def list_accounts_func(): 15 | list_of_accounts_id = [] 16 | try: 17 | list_accounts = boto3.client( 18 | 'organizations').list_accounts() 19 | list_of_accounts = list_accounts['Accounts'] 20 | while 'NextToken' in list_accounts: 21 | print('tem next token') 22 | list_accounts = boto3.client('organizations').list_accounts( 23 | NextToken=list_accounts['NextToken']) 24 | list_of_accounts += list_accounts['Accounts'] 25 | except ClientError as e: 26 | print('\n Could not list accounts... Error: ' + str(e)) 27 | logging.error(e) 28 | exit() 29 | for account in list_of_accounts: 30 | list_of_accounts_id.append(str(account["Id"])) 31 | return list_of_accounts_id 32 | 33 | 34 | def get_account_id(): 35 | return boto3.client('sts').get_caller_identity()['Account'] 36 | 37 | 38 | def list_func(accounts, current_account_id, menu_entry_1_list): 39 | resp = {'AlternateContact': {}} 40 | client = boto3.client('account') 41 | 42 | for x in accounts: 43 | alternate_contact_type = {} 44 | for y in menu_entry_1_list: 45 | print('Getting ' + y + ' alternate contact for ' + x + '...') 46 | try: 47 | if x == current_account_id: 48 | resp_alternate_contact = client.get_alternate_contact( 49 | AlternateContactType=y.upper()) 50 | resp_alternate_contact['AlternateContact'].pop( 51 | 'AlternateContactType') 52 | else: 53 | resp_alternate_contact = client.get_alternate_contact( 54 | AccountId=str(x), AlternateContactType=y.upper()) 55 | resp_alternate_contact['AlternateContact'].pop( 56 | 'AlternateContactType') 57 | except ClientError as e: 58 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 59 | resp_alternate_contact = {} 60 | resp_alternate_contact['AlternateContact'] = 'Null' 61 | else: 62 | print('\n') 63 | logging.error(e) 64 | return False 65 | alternate_contact_type[y] = resp_alternate_contact['AlternateContact'] 66 | resp['AlternateContact'][x] = alternate_contact_type 67 | 68 | export_to_s3 = input( 69 | '\nDo you want to export the result to an S3 bucket? (y/n): ') 70 | if export_to_s3 == 'y': 71 | s3_bucket_name = input('S3 bucket name: ') 72 | s3_object_name = 'alternate-contact-list_' + \ 73 | datetime.now().strftime("%d-%m-%Y_%H-%M-%S") + '.json' 74 | s3_client = boto3.client('s3') 75 | try: 76 | s3_client.put_object( 77 | Body=bytes(json.dumps(resp).encode('UTF-8')), 78 | Bucket=s3_bucket_name, 79 | Key=s3_object_name 80 | ) 81 | except ClientError as e: 82 | print('\n') 83 | logging.error(e) 84 | print(e) 85 | return False 86 | return True 87 | elif export_to_s3 == 'n': 88 | print('\nReturn: \n') 89 | pprint(resp['AlternateContact']) 90 | return True 91 | else: 92 | print('\nInvalid input.') 93 | return False 94 | 95 | 96 | def update_func(accounts, current_account_id, menu_entry_1_list): 97 | client = boto3.client('account') 98 | 99 | email_address = input( 100 | 'Type the email address (see the README.md file to valid patterns): ') 101 | name = input('Type the name (see the README.md file to valid patterns): ') 102 | phone_number = input( 103 | 'Type the phone number (see the README.md file to valid patterns): ') 104 | title = input( 105 | 'Type the title (see the README.md file to valid patterns): ') 106 | print('\n') 107 | 108 | for x in accounts: 109 | for y in menu_entry_1_list: 110 | print('Updating ' + y + ' alternate contact for ' + x + '...') 111 | if current_account_id == x: 112 | try: 113 | client.put_alternate_contact( 114 | AlternateContactType=y.upper(), 115 | EmailAddress=email_address, 116 | Name=name, 117 | PhoneNumber=phone_number, 118 | Title=title 119 | ) 120 | except ClientError as e: 121 | print('\n Could not update ' + y + 122 | ' alternate contact for ' + x + '... Error: ' + str(e)) 123 | logging.error(e) 124 | return False 125 | else: 126 | try: 127 | client.put_alternate_contact( 128 | AccountId=x, 129 | AlternateContactType=y.upper(), 130 | EmailAddress=email_address, 131 | Name=name, 132 | PhoneNumber=phone_number, 133 | Title=title 134 | ) 135 | except ClientError as e: 136 | print('\n Could not update ' + y + 137 | ' alternate contact for ' + x + '... Error: ' + str(e)) 138 | logging.error(e) 139 | return False 140 | return True 141 | 142 | 143 | def delete_func(accounts, current_account_id, menu_entry_1_list): 144 | client = boto3.client('account') 145 | 146 | for x in accounts: 147 | for y in menu_entry_1_list: 148 | print('Deleting ' + y + ' alternate contact for ' + x + '...') 149 | if current_account_id == x: 150 | try: 151 | client.delete_alternate_contact( 152 | AlternateContactType=y.upper() 153 | ) 154 | except ClientError as e: 155 | print('\n Could not delete ' + y + 156 | ' alternate contact for ' + x + '... Error: ' + str(e)) 157 | logging.error(e) 158 | return False 159 | else: 160 | try: 161 | client.delete_alternate_contact( 162 | AccountId=x, 163 | AlternateContactType=y.upper() 164 | ) 165 | except ClientError as e: 166 | print('\n Could not delete ' + y + 167 | ' alternate contact for ' + x + '... Error: ' + str(e)) 168 | logging.error(e) 169 | return False 170 | return True 171 | 172 | 173 | def main(): 174 | bold = '\033[1m' 175 | italic = '\033[0;3m' 176 | regular = '\033[0;0m' 177 | print(bold + '\nAWS Organizations Alternate Contact Manager') 178 | print(italic + 'Solution developed for batch management of alternate contacts. For more information, visit: https://github.com/aws-samples/aws-organizations-alternate-contact-manager\n\n' + regular) 179 | 180 | options_0 = ['List', 'Update', 'Delete'] 181 | terminal_menu_0 = TerminalMenu(options_0) 182 | menu_entry_index_0 = terminal_menu_0.show() 183 | menu_entry_0 = options_0[menu_entry_index_0] 184 | print('Action: ' + options_0[menu_entry_index_0]) 185 | 186 | if options_0[menu_entry_index_0] == 'Delete': 187 | accounts = input( 188 | 'Account ID (delete action allowed for one account at a time): ') 189 | accounts = accounts.split(',') 190 | else: 191 | accounts = input( 192 | 'Account IDs(enter a list of account ids separated by comma / all): ') 193 | if accounts == 'all': 194 | accounts = list_accounts_func() 195 | else: 196 | accounts = accounts.replace(' ', '') 197 | accounts = accounts.split(',') 198 | 199 | for x in accounts: 200 | if len(x) != 12: 201 | print("\nAccount ID " + str(x) + 202 | " is not a valid AWS Account ID.\n") 203 | exit() 204 | else: 205 | aws_org_accounts = list_accounts_func() 206 | if x not in aws_org_accounts: 207 | print("\nAccount ID " + str(x) + 208 | " does not belong to your AWS Organization.\n") 209 | exit() 210 | else: 211 | pass 212 | 213 | current_account_id = get_account_id() 214 | 215 | options_1 = ['Billing', 'Operations', 'Security', 'All'] 216 | terminal_menu_1 = TerminalMenu(options_1) 217 | menu_entry_index_1 = terminal_menu_1.show() 218 | print('Alternate contact type: ' + options_1[menu_entry_index_1] + '\n') 219 | if options_1[menu_entry_index_1] == 'All': 220 | menu_entry_1_list = ['Billing', 'Operations', 'Security'] 221 | else: 222 | menu_entry_1_list = [options_1[menu_entry_index_1]] 223 | 224 | tic = time.perf_counter() 225 | 226 | if menu_entry_0 == 'List': 227 | resp = list_func(accounts, current_account_id, menu_entry_1_list) 228 | elif menu_entry_0 == 'Update': 229 | resp = update_func(accounts, current_account_id, menu_entry_1_list) 230 | else: 231 | resp = delete_func(accounts, current_account_id, menu_entry_1_list) 232 | 233 | toc = time.perf_counter() 234 | 235 | print(f'\nCompleted successfully in {toc - tic:0.4f} seconds!\n') if resp == True else print( 236 | '\nERROR: somethig went wrong.\n') 237 | 238 | 239 | if __name__ == '__main__': 240 | main() 241 | --------------------------------------------------------------------------------