├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------