├── .gitignore ├── iam.png ├── screenshot.png ├── README.md ├── LICENSE ├── accounts.json ├── open_role_in_console.py.template ├── iam_role.svg └── create_workflow.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.alfredworkflow 2 | icons 3 | -------------------------------------------------------------------------------- /iam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/aws_console_alfred_shortcuts/main/iam.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/aws_console_alfred_shortcuts/main/screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws_console_alfred_shortcuts 2 | 3 | This is a script to help me create an [Alfred Workflow] that lets me switch between roles in the AWS console quickly. 4 | 5 | I work with a lot of [different roles] – when I pick a role using this workflow, it switches to that role in my frontmost Safari tab. 6 | For example, if I'm looking at Route 53 in account A, and I select a role in account B, then this workflow will switch me to Route 53 in account B. 7 | 8 | 9 | 10 | [different roles]: https://github.com/wellcomecollection/platform-infrastructure/tree/main/accounts 11 | [Alfred Workflow]: https://www.alfredapp.com/workflows/ 12 | 13 | 14 | 15 | ## Usage 16 | 17 | If you want to use this script yourself, you'll need Python installed. 18 | 19 | Clone this repo, update the list of roles in `accounts.json`, then run the script: 20 | 21 | ``` 22 | $ python3 create_workflow.py 23 | ``` 24 | 25 | This will create a package `aws_console_roles.alfredworkflow` in the repo; open this to get the shortcut. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /accounts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "760097843905", 4 | "role_names": [ 5 | "platform-read_only", 6 | "platform-developer", 7 | "platform-admin" 8 | ], 9 | "color": "red" 10 | }, 11 | { 12 | "id": "756629837203", 13 | "role_names": [ 14 | "catalogue-read_only", 15 | "catalogue-developer", 16 | "catalogue-admin" 17 | ], 18 | "color": "red" 19 | }, 20 | { 21 | "id": "130871440101", 22 | "role_names": [ 23 | "experience-read_only", 24 | "experience-developer", 25 | "experience-admin" 26 | ], 27 | "color": "red" 28 | }, 29 | { 30 | "id": "269807742353", 31 | "role_names": [ 32 | "reporting-read_only", 33 | "reporting-developer", 34 | "reporting-admin" 35 | ], 36 | "color": "red" 37 | }, 38 | { 39 | "id": "404315009621", 40 | "role_names": [ 41 | "digitisation-read_only", 42 | "digitisation-developer", 43 | "digitisation-admin" 44 | ], 45 | "color": "orange" 46 | }, 47 | { 48 | "id": "299497370133", 49 | "role_names": [ 50 | "workflow-read_only", 51 | "workflow-developer", 52 | "workflow-admin" 53 | ], 54 | "color": "yellow" 55 | }, 56 | { 57 | "id": "975596993436", 58 | "role_names": [ 59 | "storage-read_only", 60 | "storage-developer", 61 | "storage-admin" 62 | ], 63 | "color": "green" 64 | }, 65 | { 66 | "id": "770700576653", 67 | "role_names": [ 68 | "identity-read_only", 69 | "identity-developer", 70 | "identity-admin" 71 | ], 72 | "color": "blue" 73 | }, 74 | { 75 | "id": "653428163053", 76 | "role_names": [ 77 | "digirati-read_only", 78 | "digirati-developer", 79 | "digirati-admin" 80 | ], 81 | "color": "blue" 82 | }, 83 | { 84 | "id": "964279923020", 85 | "role_names": [ 86 | "data-read_only", 87 | "data-developer", 88 | "data-admin" 89 | ], 90 | "color": "red" 91 | } 92 | ] 93 | -------------------------------------------------------------------------------- /open_role_in_console.py.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | import sys 5 | import time 6 | import webbrowser 7 | 8 | from urllib.parse import quote_plus 9 | 10 | 11 | def get_front_url(): 12 | return subprocess.check_output([ 13 | "osascript", "-e", 14 | """ 15 | tell application "Safari" to get URL of document 1 16 | """ 17 | ]).decode("utf8").strip() 18 | 19 | 20 | def create_url(account_id, role_name, display_name, redirect_uri, color): 21 | url = ( 22 | "https://signin.aws.amazon.com/switchrole?" 23 | "account={account_id}&" 24 | "roleName={role_name}&" 25 | "displayName={display_name}&" 26 | "redirect_uri={redirect_uri}&" 27 | "color={color}" 28 | ).format( 29 | account_id=account_id, 30 | role_name=role_name, 31 | display_name=quote_plus(display_name), 32 | redirect_uri=quote_plus(redirect_uri), 33 | color=color, 34 | ) 35 | 36 | return url 37 | 38 | 39 | if __name__ == "__main__": 40 | role_name = {ROLE_NAME} 41 | account_id = {ACCOUNT_ID} 42 | color = {COLOR} 43 | display_name = {DISPLAY_NAME} 44 | 45 | redirect_uri = get_front_url() 46 | 47 | url = create_url( 48 | role_name=role_name, 49 | account_id=account_id, 50 | color=color, 51 | redirect_uri=redirect_uri, 52 | display_name=display_name, 53 | ) 54 | webbrowser.open(url) 55 | 56 | for _ in range(100): 57 | front_url = get_front_url() 58 | 59 | if front_url == url: 60 | subprocess.check_call( 61 | [ 62 | "osascript", 63 | "-e", 64 | """ 65 | tell application "Safari" 66 | tell document 1 to repeat 67 | do JavaScript "document.readyState" 68 | if the result = "complete" then exit repeat 69 | delay 0.1 70 | end repeat 71 | do JavaScript "document.getElementById('input_switchrole_button').click();" in document 1 72 | end tell 73 | """, 74 | ] 75 | ) 76 | sys.exit(0) 77 | 78 | time.sleep(0.1) 79 | -------------------------------------------------------------------------------- /iam_role.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icon-Resource/Security-Identity-and-Compliance/Res_AWS-Identity-Access-Management_Role_48_Light 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /create_workflow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import hashlib 4 | import json 5 | import os 6 | import plistlib 7 | import shutil 8 | import subprocess 9 | import tempfile 10 | import uuid 11 | 12 | 13 | class AlfredWorkflow: 14 | def __init__(self): 15 | self.metadata = { 16 | "bundleid": "alexwlchan.aws-console-roles", 17 | "category": "Internet", 18 | "connections": {}, 19 | "createdby": "@alexwlchan", 20 | "description": "Shortcuts to open roles in the AWS console", 21 | "name": "AWS console roles", 22 | "objects": [], 23 | "readme": "", 24 | "uidata": {}, 25 | "version": "1.0.0", 26 | "webaddress": "https://github.com/alexwlchan/github_alfred_shortcuts", 27 | } 28 | self.icons = {} 29 | 30 | def add_script(self, language, title, shortcut, filename, icon=None): 31 | script_types = { 32 | "shell": 0, 33 | "python": 9, 34 | } 35 | 36 | trigger_object = { 37 | "config": { 38 | "argumenttype": 0 if "{query}" in title else 2, 39 | "keyword": shortcut, 40 | "subtext": "", 41 | "text": title, 42 | "withspace": True, 43 | }, 44 | "type": "alfred.workflow.input.keyword", 45 | "uid": self.uuid("shortcut", shortcut, title), 46 | "version": 1, 47 | } 48 | 49 | with open(filename) as infile: 50 | script_body = infile.read() 51 | 52 | script_object = { 53 | "config": { 54 | "concurrently": False, 55 | "escaping": 102, 56 | "script": script_body, 57 | "scriptargtype": 1, 58 | "scriptfile": "", 59 | "type": script_types[language], 60 | }, 61 | "type": "alfred.workflow.action.script", 62 | "uid": self.uuid("script", script_body), 63 | "version": 2, 64 | } 65 | 66 | self._add_trigger_action_pair( 67 | trigger_object=trigger_object, action_object=script_object, icon=icon 68 | ) 69 | 70 | def add_aws_console_shortcuts(self, name, color, account_id): 71 | if not os.path.exists(f"icons/iam_role_{color}.png"): 72 | svg = open("iam_role.svg").read() 73 | 74 | os.makedirs("icons", exist_ok=True) 75 | 76 | with open(f"icons/iam_role_{color}.svg", "w") as outfile: 77 | outfile.write(svg.replace('fill="#BF0816"', f'fill="#{color}"')) 78 | 79 | subprocess.check_call( 80 | [ 81 | "convert", 82 | "-background", 83 | "none", 84 | "-density", 85 | "500", 86 | f"icons/iam_role_{color}.svg", 87 | f"icons/iam_role_{color}.png", 88 | ] 89 | ) 90 | 91 | icon = f"icons/iam_role_{color}.png" 92 | 93 | script_base = open("open_role_in_console.py.template").read() 94 | 95 | script_code = ( 96 | script_base.replace("{ROLE_NAME}", repr(f"{name}")) 97 | .replace("{COLOR}", repr(color)) 98 | .replace("{ACCOUNT_ID}", repr(account_id)) 99 | .replace("{DISPLAY_NAME}", repr(name)) 100 | ) 101 | 102 | _, script_tmp_file = tempfile.mkstemp() 103 | open(script_tmp_file, "w").write(script_code) 104 | 105 | self.add_script( 106 | language="python", 107 | title=f"Open AWS role {name}", 108 | shortcut=name, 109 | filename=os.path.abspath(script_tmp_file), 110 | icon=icon, 111 | ) 112 | 113 | os.unlink(script_tmp_file) 114 | 115 | def uuid(self, *args): 116 | assert len(args) > 0 117 | md5 = hashlib.md5() 118 | for a in args: 119 | md5.update(a.encode("utf8")) 120 | 121 | # Quick check we don't have colliding UUIDs. 122 | if not hasattr(self, "_md5s"): 123 | self._md5s = {} 124 | hex_digest = md5.hexdigest() 125 | assert hex_digest not in self._md5s, (args, self._md5s[hex_digest]) 126 | self._md5s[hex_digest] = args 127 | 128 | return str(uuid.UUID(hex=hex_digest)).upper() 129 | 130 | def _add_trigger_action_pair(self, trigger_object, action_object, icon): 131 | self.metadata["objects"].append(trigger_object) 132 | self.metadata["objects"].append(action_object) 133 | 134 | self.icons[trigger_object["uid"]] = icon 135 | 136 | if not hasattr(self, "idx"): 137 | self.idx = 0 138 | 139 | self.metadata["uidata"][trigger_object["uid"]] = { 140 | "xpos": 150, 141 | "ypos": 50 + 120 * self.idx, 142 | } 143 | self.metadata["uidata"][action_object["uid"]] = { 144 | "xpos": 600, 145 | "ypos": 50 + 120 * self.idx, 146 | } 147 | self.idx += 1 148 | 149 | self.metadata["connections"][trigger_object["uid"]] = [ 150 | { 151 | "destinationuid": action_object["uid"], 152 | "modifiers": 0, 153 | "modifiersubtext": "", 154 | "vitoclose": False, 155 | }, 156 | ] 157 | 158 | def assemble_package(self, name): 159 | with tempfile.TemporaryDirectory() as tmp_dir: 160 | shutil.copyfile("iam.png", os.path.join(tmp_dir, "Icon.png")) 161 | 162 | for icon_id, icon_path in self.icons.items(): 163 | shutil.copyfile(icon_path, os.path.join(tmp_dir, f"{icon_id}.png")) 164 | 165 | plist_path = os.path.join(tmp_dir, "Info.plist") 166 | plistlib.dump(self.metadata, open(plist_path, "wb")) 167 | 168 | shutil.make_archive( 169 | base_name=f"{name}.alfredworkflow", format="zip", root_dir=tmp_dir 170 | ) 171 | shutil.move(f"{name}.alfredworkflow.zip", f"{name}.alfredworkflow") 172 | 173 | 174 | if __name__ == "__main__": 175 | workflow = AlfredWorkflow() 176 | 177 | color_map = { 178 | 'red': 'F2B0A9', 179 | 'orange': 'FBBF93', 180 | 'yellow': 'FAD791', 181 | 'green': 'B7CA9D', 182 | 'blue': '99BCE3', 183 | } 184 | 185 | for account in json.load(open("accounts.json")): 186 | for name in account['role_names']: 187 | workflow.add_aws_console_shortcuts(account_id=account['id'], name=name, color=color_map[account['color']]) 188 | 189 | workflow.assemble_package(name="aws_console_roles") 190 | --------------------------------------------------------------------------------