lupaxa-aws-authenticate 0.1.0__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.
@@ -0,0 +1,22 @@
1
+ # OS
2
+ .DS_Store
3
+
4
+ # Cursor
5
+ .superpowers/
6
+ cursor-docs
7
+
8
+ # Make
9
+ .makefiles/
10
+
11
+ # MkDocs
12
+ site/
13
+
14
+ # Python
15
+ .venv/
16
+ __pycache__/
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ .pytest_cache/
20
+ dist/
21
+ build/
22
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) The Lupaxa Project
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.
@@ -0,0 +1,488 @@
1
+ Metadata-Version: 2.5
2
+ Name: lupaxa-aws-authenticate
3
+ Version: 0.1.0
4
+ Summary: Resolve any AWS credential source into one boto3 session.
5
+ Project-URL: Homepage, https://github.com/lupaxa-aws-toolbox/aws-authenticate
6
+ Project-URL: Repository, https://github.com/lupaxa-aws-toolbox/aws-authenticate
7
+ Project-URL: Issues, https://github.com/lupaxa-aws-toolbox/aws-authenticate/issues
8
+ Author: The Lupaxa Project
9
+ Maintainer: The Lupaxa Project
10
+ License-Expression: MIT
11
+ License-File: LICENCE
12
+ Keywords: authentication,aws,boto3
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development
23
+ Requires-Python: >=3.13
24
+ Requires-Dist: boto3<2,>=1.35.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: hatch>=1.12.0; extra == 'dev'
27
+ Requires-Dist: mypy<2.0.0,>=1.17.0; extra == 'dev'
28
+ Requires-Dist: pytest-cov<7.0.0,>=6.2.0; extra == 'dev'
29
+ Requires-Dist: pytest<9.0.0,>=8.4.0; extra == 'dev'
30
+ Requires-Dist: ruff<1.0.0,>=0.12.0; extra == 'dev'
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest-cov<7.0.0,>=6.2.0; extra == 'test'
33
+ Requires-Dist: pytest<9.0.0,>=8.4.0; extra == 'test'
34
+ Description-Content-Type: text/markdown
35
+
36
+ <p align="center">
37
+ <a href="https://github.com/lupaxa-aws-toolbox">
38
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/aws-toolbox/readme-logo.png" alt="AWS Toolbox" />
39
+ </a>
40
+ </p>
41
+
42
+ <h1 align="center">Aws Authenticate</h1>
43
+
44
+ Resolve AWS credentials and hand back one boto3 session. Callers do not branch on how the keys were obtained. Every constructor returns an `AuthenticatedSession`.
45
+
46
+ The session is a boto3 `Session`. It adds two attributes:
47
+
48
+ - `method` is the credential source that won. It is fixed when the session is created. A later refresh does not change it.
49
+ - `credentials` is a fresh `AwsCredentials` snapshot: `access_key_id`, `secret_access_key`, and `token` (`None` when there is no session token). Read it again after a refresh.
50
+
51
+ `method` is one of `explicit`, `env`, `shared-credentials-file`, `assume-role`, `assume-role-with-web-identity`, `sso`, `saml`, `custom-process`, `iam-role`, or `container-role`.
52
+ `from_chain` and `from_profile` keep whichever of those botocore selects.
53
+
54
+ An explicit constructor uses only the source you name. `from_chain` and `from_profile` use botocore's standard chain, and they open a browser once when an IAM Identity Center token is missing or expired.
55
+
56
+ Requires Python 3.13 or newer.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install lupaxa-aws-authenticate
62
+ ```
63
+
64
+ ```python
65
+ from lupaxa.aws_authenticate import AuthenticatedSession
66
+ ```
67
+
68
+ Pass `region=` when the source does not already carry one. EC2 and RDS clients need a region.
69
+
70
+ ## Errors
71
+
72
+ Authentication failures subclass `AuthenticationError`. The original botocore or STS error is `__cause__`.
73
+ A missing or blank argument raises `ValueError` before any AWS call.
74
+ API calls you make afterwards raise boto3's own errors.
75
+ This library does not call `sts:GetCallerIdentity`.
76
+
77
+ - `CredentialsNotFoundError` means the source has no keys: unknown profile, empty chain, blank SAML assertion, missing web identity file, unreachable instance metadata or container endpoint, or a credential process with no keys.
78
+ - `LoginError` means IAM Identity Center login is denied, cancelled, or times out, or no browser can be opened.
79
+ - `MfaRequiredError` means assume-role was given `mfa_serial` and no code, and there is no terminal or the answer is empty. It also means STS says MFA is required and no code was sent.
80
+ - `RoleAssumptionError` means STS or IAM Identity Center rejects the role, including a wrong MFA code, external id, or duration.
81
+ - `ValueError` means a required argument is missing or blank, such as a role ARN, a source session, an access key, or a process command. It is also raised when web identity is given both `token` and `token_file`, or neither.
82
+
83
+ ## Static Keys
84
+
85
+ `method` is `explicit`. Only the keys you pass are used.
86
+
87
+ ### Example
88
+
89
+ ```python
90
+ from lupaxa.aws_authenticate import AuthenticatedSession
91
+
92
+ session = AuthenticatedSession.from_keys(
93
+ "AKIAIOSFODNN7EXAMPLE",
94
+ "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
95
+ session_token=None,
96
+ region="eu-west-1",
97
+ )
98
+ ```
99
+
100
+ ### Config
101
+
102
+ No config file and no environment variables. A whitespace-only `session_token` is stored as `None`.
103
+
104
+ ## Environment Variables
105
+
106
+ `method` is `env`. Only `EnvProvider` is consulted.
107
+
108
+ ### Example
109
+
110
+ ```python
111
+ from lupaxa.aws_authenticate import AuthenticatedSession
112
+
113
+ session = AuthenticatedSession.from_environment(region="eu-west-1")
114
+ ```
115
+
116
+ ### Config
117
+
118
+ ```bash
119
+ export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
120
+ export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
121
+ export AWS_SESSION_TOKEN= # optional
122
+ export AWS_DEFAULT_REGION=eu-west-1
123
+ ```
124
+
125
+ `AWS_PROFILE` and the shared credentials file are ignored by this constructor.
126
+
127
+ ## Shared Profile and the Default Chain
128
+
129
+ `from_profile` checks the name, strips surrounding spaces, and calls `from_chain`.
130
+ `from_chain()` with no profile uses the default credential chain: environment variables, the shared credentials file, a web identity token, IAM Identity Center, a credential process, container credentials, then instance metadata.
131
+
132
+ When the winning provider is a shared credentials profile, `method` is `shared-credentials-file`. Otherwise `method` is whatever botocore sets.
133
+
134
+ If the profile's IAM Identity Center token is missing or expired, the library opens a browser, waits up to 10 minutes, writes `~/.aws/sso/cache`, and resolves once more.
135
+
136
+ ### Example
137
+
138
+ ```python
139
+ from lupaxa.aws_authenticate import AuthenticatedSession
140
+
141
+ session = AuthenticatedSession.from_profile("prod", region="eu-west-1")
142
+ default_session = AuthenticatedSession.from_chain(region="eu-west-1")
143
+ ```
144
+
145
+ `region=` overrides the region on the botocore session. Omit it to keep the region from the profile or from `AWS_DEFAULT_REGION`.
146
+
147
+ ### Config
148
+
149
+ Default files are `~/.aws/credentials` and `~/.aws/config`. Override them with `AWS_SHARED_CREDENTIALS_FILE` and `AWS_CONFIG_FILE`.
150
+
151
+ `~/.aws/credentials`:
152
+
153
+ ```ini
154
+ [default]
155
+ aws_access_key_id = AKIAIOSFODNN7EXAMPLE
156
+ aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
157
+
158
+ [prod]
159
+ aws_access_key_id = AKIAIOSFODNN7EXAMPLE
160
+ aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
161
+ aws_session_token = IQoJb3JpZ2luX2VjEExample
162
+ ```
163
+
164
+ `~/.aws/config`:
165
+
166
+ ```ini
167
+ [default]
168
+ region = eu-west-1
169
+
170
+ [profile prod]
171
+ region = eu-west-1
172
+ ```
173
+
174
+ A profile that assumes a role, uses SSO, or runs a credential process is valid here too. Those shapes are shown under Assume Role, IAM Identity Center, and Credential Process. `from_profile("prod")` follows whichever shape that profile has.
175
+
176
+ ## Assume Role
177
+
178
+ `method` is `assume-role`. The source session supplies the keys that call STS. This constructor does not read `~/.aws/config`.
179
+
180
+ Omitted `role_session_name` is `aws-authenticate`. Omitted `duration_seconds` is left out of the STS call, so AWS applies its default. `ExternalId` and `SerialNumber` are sent only when you pass them.
181
+
182
+ If `mfa_serial` is set and `mfa_code` is omitted, the library prompts on the terminal.
183
+ A code without `mfa_serial` raises `ValueError`.
184
+ The code is not placed in the STS arguments up front. Botocore asks the prompter when `SerialNumber` is set.
185
+
186
+ ### Example
187
+
188
+ ```python
189
+ from lupaxa.aws_authenticate import AuthenticatedSession
190
+
191
+ source = AuthenticatedSession.from_profile("prod")
192
+ session = AuthenticatedSession.from_assume_role(
193
+ source,
194
+ "arn:aws:iam::123456789012:role/Admin",
195
+ mfa_serial="arn:aws:iam::111122223333:mfa/ada",
196
+ external_id="outside",
197
+ duration_seconds=3600,
198
+ role_session_name="inventory",
199
+ region="eu-west-1",
200
+ )
201
+ ```
202
+
203
+ Pass `mfa_code="123456"` when the process has no terminal.
204
+
205
+ ### Config
206
+
207
+ Use this profile with `from_profile` when you want botocore to assume the role from the config file instead of calling `from_assume_role`:
208
+
209
+ ```ini
210
+ [profile prod-admin]
211
+ role_arn = arn:aws:iam::123456789012:role/Admin
212
+ source_profile = prod
213
+ mfa_serial = arn:aws:iam::111122223333:mfa/ada
214
+ external_id = outside
215
+ duration_seconds = 3600
216
+ role_session_name = inventory
217
+ region = eu-west-1
218
+ ```
219
+
220
+ ## Web Identity
221
+
222
+ `method` is `assume-role-with-web-identity`. Pass exactly one of `token` or `token_file`.
223
+ The file is read as UTF-8 at construction, and later refreshes read it again.
224
+ An empty file, a missing file, or an unreadable path raises `CredentialsNotFoundError`.
225
+
226
+ Omitted `role_session_name` is `aws-authenticate`. Omitted `duration_seconds` is left out of the STS call.
227
+
228
+ ### Example
229
+
230
+ ```python
231
+ from lupaxa.aws_authenticate import AuthenticatedSession
232
+
233
+ session = AuthenticatedSession.from_web_identity(
234
+ "arn:aws:iam::123456789012:role/Eks",
235
+ token_file="/var/run/secrets/eks.amazonaws.com/serviceaccount/token",
236
+ region="eu-west-1",
237
+ )
238
+ ```
239
+
240
+ A token you already hold:
241
+
242
+ ```python
243
+ session = AuthenticatedSession.from_web_identity(
244
+ "arn:aws:iam::123456789012:role/Eks",
245
+ token=open("token.jwt", encoding="utf-8").read(),
246
+ region="eu-west-1",
247
+ )
248
+ ```
249
+
250
+ ### Config
251
+
252
+ This constructor ignores `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE`. The default chain does honour them, so `from_chain()` on EKS picks them up with no arguments.
253
+
254
+ The same data as a profile, for `from_profile`:
255
+
256
+ ```ini
257
+ [profile eks]
258
+ role_arn = arn:aws:iam::123456789012:role/Eks
259
+ web_identity_token_file = /var/run/secrets/eks.amazonaws.com/serviceaccount/token
260
+ role_session_name = eks
261
+ region = eu-west-1
262
+ ```
263
+
264
+ ## SAML
265
+
266
+ `method` is `saml`. You pass a base64 SAML assertion, the role ARN, and the SAML provider ARN.
267
+ There is no identity-provider browser.
268
+ A blank assertion raises `CredentialsNotFoundError` before STS is called.
269
+ Omitted `duration_seconds` is left out of the STS call.
270
+
271
+ ### Example
272
+
273
+ ```python
274
+ from lupaxa.aws_authenticate import AuthenticatedSession
275
+
276
+ assertion = open("assertion.txt", encoding="utf-8").read()
277
+ session = AuthenticatedSession.from_saml(
278
+ "arn:aws:iam::123456789012:role/SamlAdmin",
279
+ "arn:aws:iam::123456789012:saml-provider/Example",
280
+ assertion,
281
+ duration_seconds=3600,
282
+ region="eu-west-1",
283
+ )
284
+ ```
285
+
286
+ ### Config
287
+
288
+ No config file. The assertion comes from your identity provider. The two ARNs are the role and the IAM SAML provider that trusts that provider.
289
+
290
+ ## Credential Process
291
+
292
+ `method` is `custom-process`. The command is the only source. Environment keys set beside it are ignored.
293
+
294
+ The process must print a JSON document on stdout:
295
+
296
+ ```json
297
+ {
298
+ "Version": 1,
299
+ "AccessKeyId": "ASIAEXAMPLE",
300
+ "SecretAccessKey": "secret",
301
+ "SessionToken": "token",
302
+ "Expiration": "2999-01-01T00:00:00Z"
303
+ }
304
+ ```
305
+
306
+ `SessionToken` and `Expiration` are optional. A failing process, or JSON with no keys, raises `CredentialsNotFoundError`.
307
+
308
+ ### Example
309
+
310
+ ```python
311
+ from lupaxa.aws_authenticate import AuthenticatedSession
312
+
313
+ session = AuthenticatedSession.from_process(
314
+ "/usr/local/bin/issue-keys --json",
315
+ region="eu-west-1",
316
+ )
317
+ ```
318
+
319
+ ### Config
320
+
321
+ The same command as a profile, for `from_profile`:
322
+
323
+ ```ini
324
+ [profile tools]
325
+ credential_process = /usr/local/bin/issue-keys --json
326
+ region = eu-west-1
327
+ ```
328
+
329
+ ## Instance Metadata
330
+
331
+ `method` is `iam-role`. The session uses the IAM role attached to the EC2 instance. An empty response from the metadata service raises `CredentialsNotFoundError`.
332
+
333
+ ### Example
334
+
335
+ ```python
336
+ from lupaxa.aws_authenticate import AuthenticatedSession
337
+
338
+ session = AuthenticatedSession.from_instance_metadata(region="eu-west-1")
339
+ ```
340
+
341
+ ### Config
342
+
343
+ No config file. The instance needs an instance profile. Leave `AWS_EC2_METADATA_DISABLED` unset. This constructor does not change that variable. `from_chain()` skips instance metadata when `AWS_EC2_METADATA_DISABLED=true`.
344
+
345
+ ## Container Credentials
346
+
347
+ `method` is `container-role`. Used for ECS, EKS pods that use the container credential endpoint, and App Runner. If neither URI variable is set, this raises `CredentialsNotFoundError`.
348
+
349
+ ### Example
350
+
351
+ ```python
352
+ from lupaxa.aws_authenticate import AuthenticatedSession
353
+
354
+ session = AuthenticatedSession.from_container(region="eu-west-1")
355
+ ```
356
+
357
+ ### Config
358
+
359
+ The runtime sets one of these. A relative URI is the path on `169.254.170.2`. A full URI is an absolute URL, and it may need an authorization token.
360
+
361
+ ```bash
362
+ export AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/task-id
363
+ # or
364
+ export AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.2/v1/credentials
365
+ export AWS_CONTAINER_AUTHORIZATION_TOKEN=token
366
+ # or, instead of the token value:
367
+ export AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token
368
+ ```
369
+
370
+ ## IAM Identity Center
371
+
372
+ `method` is `sso`. `from_sso` takes the start URL, the Identity Center region, the account id, and the role name. It does not read your config file. It writes a temporary profile, then resolves only the SSO provider.
373
+
374
+ When `~/.aws/sso/cache` has no usable token, the library registers a public client and opens the browser for an authorization-code login with PKCE.
375
+ It waits on `127.0.0.1` for up to 10 minutes.
376
+ The token is cached under `~/.aws/sso/cache`. The next call reuses it until it expires, then logs in again.
377
+ A denied, cancelled, or timed-out login raises `LoginError`.
378
+
379
+ `from_profile` and `from_chain` do the same login when the profile they resolve is an SSO profile.
380
+
381
+ ### Example
382
+
383
+ ```python
384
+ from lupaxa.aws_authenticate import AuthenticatedSession
385
+
386
+ session = AuthenticatedSession.from_sso(
387
+ "https://my-org.awsapps.com/start",
388
+ "eu-west-1",
389
+ "123456789012",
390
+ "ReadOnly",
391
+ region="eu-west-1",
392
+ )
393
+ ```
394
+
395
+ ### Config
396
+
397
+ No config file is required for `from_sso`. To select the same role with `from_profile`, use an `sso-session` block:
398
+
399
+ ```ini
400
+ [sso-session work]
401
+ sso_start_url = https://my-org.awsapps.com/start
402
+ sso_region = eu-west-1
403
+ sso_registration_scopes = sso:account:access
404
+
405
+ [profile work-readonly]
406
+ sso_session = work
407
+ sso_account_id = 123456789012
408
+ sso_role_name = ReadOnly
409
+ region = eu-west-1
410
+ ```
411
+
412
+ A legacy profile without an `sso-session` block also works with `from_profile`:
413
+
414
+ ```ini
415
+ [profile work-readonly]
416
+ sso_start_url = https://my-org.awsapps.com/start
417
+ sso_region = eu-west-1
418
+ sso_account_id = 123456789012
419
+ sso_role_name = ReadOnly
420
+ region = eu-west-1
421
+ ```
422
+
423
+ ## Example Scripts
424
+
425
+ Any constructor can replace the session line. The client calls are ordinary boto3 calls.
426
+
427
+ ### List EC2 Instances
428
+
429
+ Uses a shared profile. Swap `from_profile` for `from_environment`, `from_instance_metadata`, or `from_container` when the process is already running with those credentials.
430
+
431
+ ```python
432
+ """List running EC2 instances."""
433
+
434
+ from lupaxa.aws_authenticate import AuthenticatedSession
435
+
436
+ session = AuthenticatedSession.from_profile("prod", region="eu-west-1")
437
+ ec2 = session.client("ec2")
438
+ pages = ec2.get_paginator("describe_instances").paginate(
439
+ Filters=[{"Name": "instance-state-name", "Values": ["running"]}],
440
+ )
441
+ for page in pages:
442
+ for reservation in page["Reservations"]:
443
+ for instance in reservation["Instances"]:
444
+ name = next(
445
+ (
446
+ tag["Value"]
447
+ for tag in instance.get("Tags", [])
448
+ if tag["Key"] == "Name"
449
+ ),
450
+ "",
451
+ )
452
+ print(
453
+ instance["InstanceId"],
454
+ name,
455
+ instance.get("PrivateIpAddress", ""),
456
+ )
457
+ ```
458
+
459
+ ### List RDS Instances
460
+
461
+ Uses IAM Identity Center. The first run opens a browser when the cached token is missing or expired.
462
+
463
+ ```python
464
+ """List RDS DB instances."""
465
+
466
+ from lupaxa.aws_authenticate import AuthenticatedSession
467
+
468
+ session = AuthenticatedSession.from_sso(
469
+ "https://my-org.awsapps.com/start",
470
+ "eu-west-1",
471
+ "123456789012",
472
+ "ReadOnly",
473
+ region="eu-west-1",
474
+ )
475
+ rds = session.client("rds")
476
+ pages = rds.get_paginator("describe_db_instances").paginate()
477
+ for page in pages:
478
+ for database in page["DBInstances"]:
479
+ print(
480
+ database["DBInstanceIdentifier"],
481
+ database["Engine"],
482
+ database["DBInstanceStatus"],
483
+ )
484
+ ```
485
+
486
+ <a href="https://github.com/the-lupaxa-project">
487
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
488
+ </a>