fogies 0.0.0.dev6__tar.gz → 0.0.0.dev7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/PKG-INFO +1 -1
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/pyproject.toml +1 -1
- fogies-0.0.0.dev7/src/fogies/pyfogies.py +37 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/access_key.py +279 -286
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/terraform.py +46 -21
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/terraform_backend.py +33 -12
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/backend.py +222 -222
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/aws_environ.py +11 -4
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/terraform.py +77 -7
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/README.md +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/__init__.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/aws_access_key.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/includes/templates/aws.toml.template +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/includes/templates/backend_status.toml.template +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/includes/templates/poetry.toml.template +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/py.typed +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/ready_poll.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/retry.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/__init__.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/format.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/lint.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/poetry.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tasks/test.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/templates.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/__init__.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/access_key_permissions.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/alb.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/cloudwatch.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/ecr.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/ecs.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/hosted_zone.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/terraform/network.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/__init__.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/boto.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/command.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/environ.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/ollama.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/tools/terraform_backend.py +0 -0
- {fogies-0.0.0.dev6 → fogies-0.0.0.dev7}/src/fogies/typing.py +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Facts about the installed pyfogies package itself."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
# importlib.metadata reports the PEP 440 canonical form, which is how Poetry
|
|
7
|
+
# normalizes the SemVer-hyphenated version string written in pyproject.toml
|
|
8
|
+
# and CHANGELOG.md (e.g. "0.0.0-dev.6" becomes "0.0.0.dev6"). These are the
|
|
9
|
+
# only two forms this project's own versions ever take.
|
|
10
|
+
_FINAL_RELEASE_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
|
11
|
+
_DEV_RELEASE_RE = re.compile(r"^(?P<base>\d+\.\d+\.\d+)\.dev(?P<dev>\d+)$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pyfogies_version() -> str:
|
|
15
|
+
"""The installed pyfogies version, in this project's own format (e.g. "0.0.0-dev.6").
|
|
16
|
+
|
|
17
|
+
Converts the PEP 440 form importlib.metadata reports back to the
|
|
18
|
+
SemVer-hyphenated form used everywhere else in the project. Raises if
|
|
19
|
+
the installed version doesn't match either expected shape, rather than
|
|
20
|
+
guessing at an unfamiliar one.
|
|
21
|
+
"""
|
|
22
|
+
installed_version = importlib.metadata.version("fogies")
|
|
23
|
+
|
|
24
|
+
if _FINAL_RELEASE_RE.match(installed_version):
|
|
25
|
+
return installed_version
|
|
26
|
+
|
|
27
|
+
dev_release_match = _DEV_RELEASE_RE.match(installed_version)
|
|
28
|
+
if dev_release_match:
|
|
29
|
+
return "{}-dev.{}".format(
|
|
30
|
+
dev_release_match.group("base"), dev_release_match.group("dev")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"Unrecognized pyfogies version '{}'; expected 'X.Y.Z' or 'X.Y.Z.devN'".format(
|
|
35
|
+
installed_version
|
|
36
|
+
)
|
|
37
|
+
)
|
|
@@ -1,286 +1,279 @@
|
|
|
1
|
-
"""Tasks for managing IAM users and access keys."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import contextlib
|
|
6
|
-
import getpass
|
|
7
|
-
from collections.abc import Callable, Generator
|
|
8
|
-
from typing import cast
|
|
9
|
-
|
|
10
|
-
from invoke.collection import Collection
|
|
11
|
-
from invoke.context import Context
|
|
12
|
-
from invoke.tasks import Task, task
|
|
13
|
-
|
|
14
|
-
import fogies.aws_access_key as aws_access_key
|
|
15
|
-
from fogies.tools.aws_environ import (
|
|
16
|
-
|
|
17
|
-
AwsProfile,
|
|
18
|
-
aws_environ_from_profile,
|
|
19
|
-
)
|
|
20
|
-
from fogies.typing import boto_client_sts
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
keys
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
collection = Collection("access-key")
|
|
281
|
-
collection.add_task(get_task_create(aws_environ_factory=aws_environ_factory))
|
|
282
|
-
collection.add_task(get_task_delete(aws_environ_factory=aws_environ_factory))
|
|
283
|
-
collection.add_task(get_task_delete_key(aws_environ_factory=aws_environ_factory))
|
|
284
|
-
collection.add_task(get_task_list(aws_environ_factory=aws_environ_factory))
|
|
285
|
-
collection.add_task(get_task_rotate_key(aws_environ_factory=aws_environ_factory))
|
|
286
|
-
return collection
|
|
1
|
+
"""Tasks for managing IAM users and access keys."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import getpass
|
|
7
|
+
from collections.abc import Callable, Generator
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from invoke.collection import Collection
|
|
11
|
+
from invoke.context import Context
|
|
12
|
+
from invoke.tasks import Task, task
|
|
13
|
+
|
|
14
|
+
import fogies.aws_access_key as aws_access_key
|
|
15
|
+
from fogies.tools.aws_environ import (
|
|
16
|
+
AwsEnvironFactory,
|
|
17
|
+
AwsProfile,
|
|
18
|
+
aws_environ_from_profile,
|
|
19
|
+
)
|
|
20
|
+
from fogies.typing import boto_client_sts
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _prompt_admin_credentials() -> tuple[str, str]:
|
|
24
|
+
"""Prompt for admin credentials interactively; credentials are never written anywhere."""
|
|
25
|
+
access_key_id = input("Admin Access Key ID: ").strip()
|
|
26
|
+
secret_access_key = getpass.getpass("Admin Secret Access Key: ").strip()
|
|
27
|
+
return access_key_id, secret_access_key
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextlib.contextmanager
|
|
31
|
+
def _resolve_admin_environ(
|
|
32
|
+
*, aws_environ_factory: AwsEnvironFactory | None, prompt: bool
|
|
33
|
+
) -> Generator[str]:
|
|
34
|
+
"""Enter the configured AWS environment, or prompt for admin credentials.
|
|
35
|
+
|
|
36
|
+
Prompts if aws_environ_factory is None, or if prompt is True (e.g. to use
|
|
37
|
+
different/elevated credentials for a single run). Prompted credentials
|
|
38
|
+
are never written anywhere. Yields the active access key ID.
|
|
39
|
+
"""
|
|
40
|
+
if aws_environ_factory is not None and not prompt:
|
|
41
|
+
with aws_environ_factory() as env:
|
|
42
|
+
yield env.aws_access_key_id
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
access_key_id, secret_access_key = _prompt_admin_credentials()
|
|
46
|
+
profile = AwsProfile(
|
|
47
|
+
name="prompt-admin",
|
|
48
|
+
aws_access_key_id=access_key_id,
|
|
49
|
+
aws_secret_access_key=secret_access_key,
|
|
50
|
+
)
|
|
51
|
+
# try/except wraps only entering the context manager (where validation
|
|
52
|
+
# happens), not the yield below — catching ValueError around the yield
|
|
53
|
+
# too would risk mis-catching an unrelated error raised by the task body.
|
|
54
|
+
with contextlib.ExitStack() as stack:
|
|
55
|
+
try:
|
|
56
|
+
env = stack.enter_context(
|
|
57
|
+
aws_environ_from_profile(profile=profile, raise_if_exists=False)
|
|
58
|
+
)
|
|
59
|
+
except ValueError as exc:
|
|
60
|
+
raise SystemExit(str(exc)) from exc
|
|
61
|
+
yield env.aws_access_key_id
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _caller_username() -> str:
|
|
65
|
+
"""Return the IAM username that owns the active credentials, via STS."""
|
|
66
|
+
arn = boto_client_sts().get_caller_identity()["Arn"]
|
|
67
|
+
# ARN format for IAM users: arn:aws:iam::123456789012:user/username
|
|
68
|
+
return arn.split("/")[-1]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _print_toml_block(*, profile: AwsProfile) -> None:
|
|
72
|
+
"""Print a TOML block suitable for pasting into aws.toml."""
|
|
73
|
+
print()
|
|
74
|
+
print("[{}]".format(profile.name))
|
|
75
|
+
print('aws_access_key_id = "{}"'.format(profile.aws_access_key_id))
|
|
76
|
+
print('aws_secret_access_key = "{}"'.format(profile.aws_secret_access_key))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _print_user_key_overview(
|
|
80
|
+
*, username: str, keys: aws_access_key.IamAccessKeys
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Print an IAM user's key overview: current/previous key details, or a no-keys note.
|
|
83
|
+
|
|
84
|
+
Shared by `list` (once per user) and `delete-key`'s confirmation prompt
|
|
85
|
+
(for the one user being acted on), so both show the same information.
|
|
86
|
+
"""
|
|
87
|
+
print(username)
|
|
88
|
+
for label, key in (("current", keys.current), ("previous", keys.previous)):
|
|
89
|
+
if key is None:
|
|
90
|
+
continue
|
|
91
|
+
created_str = key.created.strftime("%Y-%m-%d") if key.created else "unknown"
|
|
92
|
+
last_used_str = key.last_used.strftime("%Y-%m-%d") if key.last_used else "never"
|
|
93
|
+
print(
|
|
94
|
+
" {} {} | {} | created {} | last used {}".format(
|
|
95
|
+
label, key.key_id, key.status, created_str, last_used_str
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
if keys.current is None:
|
|
99
|
+
print(" (no access keys)")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_task_create(
|
|
103
|
+
*, aws_environ_factory: AwsEnvironFactory | None = None
|
|
104
|
+
) -> Task[Callable[..., None]]:
|
|
105
|
+
@task(name="create") # pyright: ignore[reportUntypedFunctionDecorator]
|
|
106
|
+
def task_create(context: Context, *, prompt: bool = False, username: str) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Create an IAM user and an associated access key.
|
|
109
|
+
|
|
110
|
+
Flags:
|
|
111
|
+
--prompt Prompt for admin credentials.
|
|
112
|
+
--username IAM username.
|
|
113
|
+
"""
|
|
114
|
+
_ = context
|
|
115
|
+
with _resolve_admin_environ(
|
|
116
|
+
aws_environ_factory=aws_environ_factory, prompt=prompt
|
|
117
|
+
):
|
|
118
|
+
new_profile = aws_access_key.create_user(username=username)
|
|
119
|
+
print("Created IAM user '{}'.".format(username))
|
|
120
|
+
print("Created access key {}.".format(new_profile.aws_access_key_id))
|
|
121
|
+
_print_toml_block(profile=new_profile)
|
|
122
|
+
|
|
123
|
+
return cast(Task[Callable[..., None]], task_create)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_task_delete(
|
|
127
|
+
*, aws_environ_factory: AwsEnvironFactory | None = None
|
|
128
|
+
) -> Task[Callable[..., None]]:
|
|
129
|
+
@task(name="delete") # pyright: ignore[reportUntypedFunctionDecorator]
|
|
130
|
+
def task_delete(context: Context, *, prompt: bool = False, username: str) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Delete an IAM user with no remaining access keys.
|
|
133
|
+
|
|
134
|
+
Flags:
|
|
135
|
+
--prompt Prompt for admin credentials.
|
|
136
|
+
--username IAM username.
|
|
137
|
+
"""
|
|
138
|
+
_ = context
|
|
139
|
+
with _resolve_admin_environ(
|
|
140
|
+
aws_environ_factory=aws_environ_factory, prompt=prompt
|
|
141
|
+
):
|
|
142
|
+
protected_username = _caller_username()
|
|
143
|
+
confirm = (
|
|
144
|
+
input("Delete IAM user '{}'? [y/N] ".format(username)).strip().lower()
|
|
145
|
+
)
|
|
146
|
+
if confirm != "y":
|
|
147
|
+
print("Aborted.")
|
|
148
|
+
return
|
|
149
|
+
aws_access_key.delete_user(
|
|
150
|
+
username=username,
|
|
151
|
+
protected_usernames={protected_username},
|
|
152
|
+
)
|
|
153
|
+
print("Deleted IAM user '{}'.".format(username))
|
|
154
|
+
|
|
155
|
+
return cast(Task[Callable[..., None]], task_delete)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_task_delete_key(
|
|
159
|
+
*, aws_environ_factory: AwsEnvironFactory | None = None
|
|
160
|
+
) -> Task[Callable[..., None]]:
|
|
161
|
+
@task(name="delete-key") # pyright: ignore[reportUntypedFunctionDecorator]
|
|
162
|
+
def task_delete_key(
|
|
163
|
+
context: Context, *, prompt: bool = False, username: str
|
|
164
|
+
) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Delete the oldest access key for an IAM user.
|
|
167
|
+
|
|
168
|
+
Flags:
|
|
169
|
+
--prompt Prompt for admin credentials.
|
|
170
|
+
--username IAM username.
|
|
171
|
+
"""
|
|
172
|
+
_ = context
|
|
173
|
+
with _resolve_admin_environ(
|
|
174
|
+
aws_environ_factory=aws_environ_factory, prompt=prompt
|
|
175
|
+
) as admin_key_id:
|
|
176
|
+
keys = aws_access_key.get_keys(username=username)
|
|
177
|
+
target = keys.previous or keys.current
|
|
178
|
+
if target is None:
|
|
179
|
+
raise SystemExit("IAM user '{}' has no access keys.".format(username))
|
|
180
|
+
key_id = target.key_id
|
|
181
|
+
_print_user_key_overview(username=username, keys=keys)
|
|
182
|
+
print()
|
|
183
|
+
if keys.previous is None:
|
|
184
|
+
print(
|
|
185
|
+
"WARNING: This is the only key. Deleting it will revoke all access."
|
|
186
|
+
)
|
|
187
|
+
print()
|
|
188
|
+
confirm = input("Delete key {}? [y/N] ".format(key_id)).strip().lower()
|
|
189
|
+
if confirm != "y":
|
|
190
|
+
print("Aborted.")
|
|
191
|
+
return
|
|
192
|
+
aws_access_key.delete_key(
|
|
193
|
+
username=username,
|
|
194
|
+
key_id=key_id,
|
|
195
|
+
protected_key_ids={admin_key_id},
|
|
196
|
+
)
|
|
197
|
+
print("Deleted key {}.".format(key_id))
|
|
198
|
+
|
|
199
|
+
return cast(Task[Callable[..., None]], task_delete_key)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_task_list(
|
|
203
|
+
*, aws_environ_factory: AwsEnvironFactory | None = None
|
|
204
|
+
) -> Task[Callable[..., None]]:
|
|
205
|
+
@task(name="list") # pyright: ignore[reportUntypedFunctionDecorator]
|
|
206
|
+
def task_list(context: Context, *, prompt: bool = False) -> None:
|
|
207
|
+
"""
|
|
208
|
+
List all IAM users and their access keys.
|
|
209
|
+
|
|
210
|
+
Flags:
|
|
211
|
+
--prompt Prompt for admin credentials.
|
|
212
|
+
"""
|
|
213
|
+
_ = context
|
|
214
|
+
with _resolve_admin_environ(
|
|
215
|
+
aws_environ_factory=aws_environ_factory, prompt=prompt
|
|
216
|
+
):
|
|
217
|
+
users = aws_access_key.list_users()
|
|
218
|
+
if not users:
|
|
219
|
+
print("No IAM users found.")
|
|
220
|
+
return
|
|
221
|
+
for index, user in enumerate(users):
|
|
222
|
+
if index > 0:
|
|
223
|
+
print()
|
|
224
|
+
_print_user_key_overview(username=user.username, keys=user.keys)
|
|
225
|
+
|
|
226
|
+
return cast(Task[Callable[..., None]], task_list)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_task_rotate_key(
|
|
230
|
+
*, aws_environ_factory: AwsEnvironFactory | None = None
|
|
231
|
+
) -> Task[Callable[..., None]]:
|
|
232
|
+
@task(name="rotate-key") # pyright: ignore[reportUntypedFunctionDecorator]
|
|
233
|
+
def task_rotate_key(
|
|
234
|
+
context: Context, *, prompt: bool = False, username: str
|
|
235
|
+
) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Add a new access key to an existing IAM user.
|
|
238
|
+
|
|
239
|
+
Flags:
|
|
240
|
+
--prompt Prompt for admin credentials instead of using the configured AWS environment.
|
|
241
|
+
--username IAM username to rotate a key for.
|
|
242
|
+
"""
|
|
243
|
+
_ = context
|
|
244
|
+
with _resolve_admin_environ(
|
|
245
|
+
aws_environ_factory=aws_environ_factory, prompt=prompt
|
|
246
|
+
) as admin_key_id:
|
|
247
|
+
keys = aws_access_key.get_keys(username=username)
|
|
248
|
+
if keys.previous is not None:
|
|
249
|
+
_print_user_key_overview(username=username, keys=keys)
|
|
250
|
+
print()
|
|
251
|
+
confirm = (
|
|
252
|
+
input("Delete key {}? [y/N] ".format(keys.previous.key_id))
|
|
253
|
+
.strip()
|
|
254
|
+
.lower()
|
|
255
|
+
)
|
|
256
|
+
if confirm != "y":
|
|
257
|
+
print("Aborted.")
|
|
258
|
+
return
|
|
259
|
+
new_profile = aws_access_key.rotate_key(
|
|
260
|
+
username=username,
|
|
261
|
+
protected_key_ids={admin_key_id},
|
|
262
|
+
)
|
|
263
|
+
print("Created access key {}.".format(new_profile.aws_access_key_id))
|
|
264
|
+
_print_toml_block(profile=new_profile)
|
|
265
|
+
|
|
266
|
+
return cast(Task[Callable[..., None]], task_rotate_key)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_collection(
|
|
270
|
+
*, aws_environ_factory: AwsEnvironFactory | None = None
|
|
271
|
+
) -> Collection:
|
|
272
|
+
"""Get a collection of tasks for managing IAM users and access keys."""
|
|
273
|
+
collection = Collection("access-key")
|
|
274
|
+
collection.add_task(get_task_create(aws_environ_factory=aws_environ_factory))
|
|
275
|
+
collection.add_task(get_task_delete(aws_environ_factory=aws_environ_factory))
|
|
276
|
+
collection.add_task(get_task_delete_key(aws_environ_factory=aws_environ_factory))
|
|
277
|
+
collection.add_task(get_task_list(aws_environ_factory=aws_environ_factory))
|
|
278
|
+
collection.add_task(get_task_rotate_key(aws_environ_factory=aws_environ_factory))
|
|
279
|
+
return collection
|