qontract-reconcile 0.10.1rc602__py3-none-any.whl → 0.10.1rc604__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc602
3
+ Version: 0.10.1rc604
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Requires-Python: >=3.11
14
- Requires-Dist: sretoolbox ~=2.5.1
14
+ Requires-Dist: sretoolbox ~=2.5.2
15
15
  Requires-Dist: Click <9.0,>=7.0
16
16
  Requires-Dist: gql ==3.1.0
17
17
  Requires-Dist: toml <0.11.0,>=0.10.0
@@ -539,7 +539,7 @@ reconcile/typed_queries/terraform_tgw_attachments/aws_accounts.py,sha256=T5HSeyB
539
539
  reconcile/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
540
540
  reconcile/utils/aggregated_list.py,sha256=pkYoBj7WwmaNgEefETqEOFTnQMcUzHE3mdsVdzGYj60,3372
541
541
  reconcile/utils/amtool.py,sha256=JV5-to_e_FaIcvJWTKYA9d6L3LwzwijM0MjUWn83eD4,2204
542
- reconcile/utils/aws_api.py,sha256=Wy040GBQ3HrWmtxe1QAx3zl1I3phVDVjXEx_OITEcOw,69191
542
+ reconcile/utils/aws_api.py,sha256=sgmxCXYle0jjmMHPpBOAKl-kWTDWwuc0AlLO2NmDB8M,65612
543
543
  reconcile/utils/aws_helper.py,sha256=6Nfgsz0aQ97LBAJ0JBRdnPaFTAkEBSqXvCH6_pVIWdw,2006
544
544
  reconcile/utils/binary.py,sha256=3IBnwjKakHM367skPPvG6yVSQYjKt5muQlFNdoa63DU,2352
545
545
  reconcile/utils/config.py,sha256=aId5zrPjM_84u_T4yTRE_Psu3zo5-5_JCR6_7Wgv5UQ,990
@@ -618,6 +618,11 @@ reconcile/utils/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
618
618
  reconcile/utils/acs/base.py,sha256=Qih-xZ3RBJZEE291iHHlv7lUY6ShcAvSj1PA3_aTTnM,2276
619
619
  reconcile/utils/acs/policies.py,sha256=_jAz6cv8KRYtDsXjGoJgNbD8_9PUa5LSwwVlpK4A_cQ,5505
620
620
  reconcile/utils/acs/rbac.py,sha256=ugsLM9Pb7FbUbdq85E3VzXGMaB9ZovXob7tdWCxwqZ8,8808
621
+ reconcile/utils/aws_api_typed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
622
+ reconcile/utils/aws_api_typed/api.py,sha256=ICBrpoZ1Y8ShMaNOQmzPzChmw-Ffdg0tCsu97fVwZ-w,6374
623
+ reconcile/utils/aws_api_typed/iam.py,sha256=wH82lA2kUgEKR5McmyU5gB8ASfemT5xAk3Bb9cairVo,1508
624
+ reconcile/utils/aws_api_typed/organization.py,sha256=dJ7J02BNHu7UDyFa9083b92vSamIX8DtHIoF8VwEijY,3675
625
+ reconcile/utils/aws_api_typed/sts.py,sha256=5Sauncj9Fif3YDLkJYkBZrtOX0v0bGAqOmY0A5Bh9yA,1237
621
626
  reconcile/utils/cloud_resource_best_practice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
622
627
  reconcile/utils/cloud_resource_best_practice/aws_rds.py,sha256=EvE6XKLsrZ531MJptKqPht2lOETrOjySTHXk6CzMgo0,2279
623
628
  reconcile/utils/glitchtip/__init__.py,sha256=FT6iBhGqoe7KExFdbgL8AYUb64iW_4snF5__Dcl7yt0,258
@@ -704,8 +709,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
704
709
  tools/test/test_qontract_cli.py,sha256=OvalpVRfY4pNmpMaWHHYqBjV68b1eGQjX8SCyTAXb1w,3501
705
710
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
706
711
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
707
- qontract_reconcile-0.10.1rc602.dist-info/METADATA,sha256=63UVto0tsq6BwLN2ztvG4TOvP2_EXbX3IahwEhuh0HA,2349
708
- qontract_reconcile-0.10.1rc602.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
709
- qontract_reconcile-0.10.1rc602.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
710
- qontract_reconcile-0.10.1rc602.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
711
- qontract_reconcile-0.10.1rc602.dist-info/RECORD,,
712
+ qontract_reconcile-0.10.1rc604.dist-info/METADATA,sha256=8DqZZvzBIt5WiKjrw88w6gjkqWMvmmFJteu3Hskyuyo,2349
713
+ qontract_reconcile-0.10.1rc604.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
714
+ qontract_reconcile-0.10.1rc604.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
715
+ qontract_reconcile-0.10.1rc604.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
716
+ qontract_reconcile-0.10.1rc604.dist-info/RECORD,,
@@ -1,9 +1,7 @@
1
1
  import logging
2
2
  import os
3
3
  import re
4
- import textwrap
5
4
  import time
6
- from abc import ABC, abstractmethod
7
5
  from collections.abc import (
8
6
  Iterable,
9
7
  Iterator,
@@ -89,118 +87,6 @@ KeyStatus = Union[Literal["Active"], Literal["Inactive"]]
89
87
  GOVCLOUD_PARTITION = "aws-us-gov"
90
88
 
91
89
 
92
- class AWSCredentials(ABC):
93
- @abstractmethod
94
- def as_env_vars(self) -> dict[str, str]:
95
- """
96
- Returns a dictionary of environment variables that can be used to authenticate with AWS.
97
- """
98
- ...
99
-
100
- @abstractmethod
101
- def as_credentials_file(self, profile_name: str = "default") -> str:
102
- """
103
- Returns a string that can be used to write an AWS credentials file.
104
- """
105
- ...
106
-
107
- @abstractmethod
108
- def build_session(self) -> Session:
109
- """
110
- Builds an AWS session using these credentials.
111
- """
112
- ...
113
-
114
- def get_temporary_credentials(
115
- self, duration_seconds: int = 900
116
- ) -> "AWSTemporaryCredentials":
117
- """
118
- Builds temporary AWS credentials from a session. This is similar to assuming a role,
119
- in the sense that the credentials will expire after a certain amount of time.
120
-
121
- These temporary credentials have the same permissions as the session they were built from, except:
122
- - they can't be used for anything IAM related
123
- - for the STS API only the AssumeRole and GetSessionToken actions are allowed
124
- """
125
- session = self.build_session()
126
- response = session.client("sts").get_session_token(
127
- DurationSeconds=duration_seconds
128
- )
129
- tmp_creds = response["Credentials"]
130
- return AWSTemporaryCredentials(
131
- access_key_id=tmp_creds["AccessKeyId"],
132
- secret_access_key=tmp_creds["SecretAccessKey"],
133
- session_token=tmp_creds["SessionToken"],
134
- region=session.region_name,
135
- )
136
-
137
-
138
- class AWSStaticCredentials(BaseModel, AWSCredentials):
139
- """
140
- A model representing AWS credentials.
141
- """
142
-
143
- access_key_id: str
144
- secret_access_key: str
145
- region: str
146
-
147
- def as_env_vars(self) -> dict[str, str]:
148
- return {
149
- "AWS_ACCESS_KEY_ID": self.access_key_id,
150
- "AWS_SECRET_ACCESS_KEY": self.secret_access_key,
151
- "AWS_REGION": self.region,
152
- }
153
-
154
- def as_credentials_file(self, profile_name: str = "default") -> str:
155
- return textwrap.dedent(
156
- f"""\
157
- [{profile_name}]
158
- aws_access_key_id = {self.access_key_id}
159
- aws_secret_access_key = {self.secret_access_key}
160
- region = {self.region}
161
- """
162
- )
163
-
164
- def build_session(self) -> Session:
165
- return Session(
166
- aws_access_key_id=self.access_key_id,
167
- aws_secret_access_key=self.secret_access_key,
168
- region_name=self.region,
169
- )
170
-
171
-
172
- class AWSTemporaryCredentials(AWSStaticCredentials):
173
- """
174
- A model representing temporary AWS credentials.
175
- """
176
-
177
- session_token: str
178
-
179
- def as_env_vars(self) -> dict[str, str]:
180
- env_vars = super().as_env_vars()
181
- env_vars["AWS_SESSION_TOKEN"] = self.session_token
182
- return env_vars
183
-
184
- def as_credentials_file(self, profile_name: str = "default") -> str:
185
- return textwrap.dedent(
186
- f"""\
187
- [{profile_name}]
188
- aws_access_key_id = {self.access_key_id}
189
- aws_secret_access_key = {self.secret_access_key}
190
- aws_session_token = {self.session_token}
191
- region = {self.region}
192
- """
193
- )
194
-
195
- def build_session(self) -> Session:
196
- return Session(
197
- aws_access_key_id=self.access_key_id,
198
- aws_secret_access_key=self.secret_access_key,
199
- aws_session_token=self.session_token,
200
- region_name=self.region,
201
- )
202
-
203
-
204
90
  class AmiTag(BaseModel):
205
91
  name: str
206
92
  value: str
File without changes
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+ from abc import ABC, abstractmethod
5
+ from functools import cached_property
6
+ from typing import Any, TypeVar
7
+
8
+ from boto3 import Session
9
+ from botocore.client import BaseClient
10
+ from pydantic import BaseModel
11
+
12
+ import reconcile.utils.aws_api_typed.iam
13
+ import reconcile.utils.aws_api_typed.organization
14
+ import reconcile.utils.aws_api_typed.sts
15
+ from reconcile.utils.aws_api_typed.iam import AWSApiIam
16
+ from reconcile.utils.aws_api_typed.organization import AWSApiOrganizations
17
+ from reconcile.utils.aws_api_typed.sts import AWSApiSts
18
+
19
+ SubApi = TypeVar("SubApi")
20
+
21
+
22
+ class AWSCredentials(ABC):
23
+ @abstractmethod
24
+ def as_env_vars(self) -> dict[str, str]:
25
+ """
26
+ Returns a dictionary of environment variables that can be used to authenticate with AWS.
27
+ """
28
+ ...
29
+
30
+ @abstractmethod
31
+ def as_credentials_file(self, profile_name: str = "default") -> str:
32
+ """
33
+ Returns a string that can be used to write an AWS credentials file.
34
+ """
35
+ ...
36
+
37
+ @abstractmethod
38
+ def build_session(self) -> Session:
39
+ """
40
+ Builds an AWS session using these credentials.
41
+ """
42
+ ...
43
+
44
+
45
+ class AWSStaticCredentials(BaseModel, AWSCredentials):
46
+ """
47
+ A model representing AWS credentials.
48
+ """
49
+
50
+ access_key_id: str
51
+ secret_access_key: str
52
+ region: str
53
+
54
+ def as_env_vars(self) -> dict[str, str]:
55
+ return {
56
+ "AWS_ACCESS_KEY_ID": self.access_key_id,
57
+ "AWS_SECRET_ACCESS_KEY": self.secret_access_key,
58
+ "AWS_REGION": self.region,
59
+ }
60
+
61
+ def as_credentials_file(self, profile_name: str = "default") -> str:
62
+ return textwrap.dedent(
63
+ f"""\
64
+ [{profile_name}]
65
+ aws_access_key_id = {self.access_key_id}
66
+ aws_secret_access_key = {self.secret_access_key}
67
+ region = {self.region}
68
+ """
69
+ )
70
+
71
+ def build_session(self) -> Session:
72
+ return Session(
73
+ aws_access_key_id=self.access_key_id,
74
+ aws_secret_access_key=self.secret_access_key,
75
+ region_name=self.region,
76
+ )
77
+
78
+
79
+ class AWSTemporaryCredentials(AWSStaticCredentials):
80
+ """
81
+ A model representing temporary AWS credentials.
82
+ """
83
+
84
+ session_token: str
85
+
86
+ def as_env_vars(self) -> dict[str, str]:
87
+ env_vars = super().as_env_vars()
88
+ env_vars["AWS_SESSION_TOKEN"] = self.session_token
89
+ return env_vars
90
+
91
+ def as_credentials_file(self, profile_name: str = "default") -> str:
92
+ return textwrap.dedent(
93
+ f"""\
94
+ [{profile_name}]
95
+ aws_access_key_id = {self.access_key_id}
96
+ aws_secret_access_key = {self.secret_access_key}
97
+ aws_session_token = {self.session_token}
98
+ region = {self.region}
99
+ """
100
+ )
101
+
102
+ def build_session(self) -> Session:
103
+ return Session(
104
+ aws_access_key_id=self.access_key_id,
105
+ aws_secret_access_key=self.secret_access_key,
106
+ aws_session_token=self.session_token,
107
+ region_name=self.region,
108
+ )
109
+
110
+
111
+ class AWSApi:
112
+ def __init__(self, aws_credentials: AWSCredentials) -> None:
113
+ self.session = aws_credentials.build_session()
114
+ self._session_clients: list[BaseClient] = []
115
+
116
+ def __enter__(self) -> AWSApi:
117
+ return self
118
+
119
+ def __exit__(self, *exec: Any) -> None:
120
+ self.close()
121
+
122
+ def close(self) -> None:
123
+ """Close all clients created by this API instance."""
124
+ for client in self._session_clients:
125
+ client.close()
126
+ self._session_clients = []
127
+
128
+ def _init_sub_api(self, api_cls: type[SubApi]) -> SubApi:
129
+ """Return a new or cached sub api client."""
130
+ match api_cls:
131
+ case reconcile.utils.aws_api_typed.iam.AWSApiIam:
132
+ client = self.session.client("iam")
133
+ api = api_cls(client) # type: ignore # mypy bug, it doesn't recognize that api_cls is callable
134
+ case reconcile.utils.aws_api_typed.organization.AWSApiOrganizations:
135
+ client = self.session.client("organizations")
136
+ api = api_cls(client)
137
+ case reconcile.utils.aws_api_typed.sts.AWSApiSts:
138
+ client = self.session.client("sts")
139
+ api = api_cls(client)
140
+ case _:
141
+ raise ValueError(f"Unknown API class: {api_cls}")
142
+
143
+ self._session_clients.append(client)
144
+ return api
145
+
146
+ @cached_property
147
+ def sts(self) -> AWSApiSts:
148
+ """Return an AWS STS Api client."""
149
+ return self._init_sub_api(AWSApiSts)
150
+
151
+ @cached_property
152
+ def organizations(self) -> AWSApiOrganizations:
153
+ """Return an AWS Organizations Api client."""
154
+ return self._init_sub_api(AWSApiOrganizations)
155
+
156
+ @cached_property
157
+ def iam(self) -> AWSApiIam:
158
+ """Return an AWS IAM Api client."""
159
+ return self._init_sub_api(AWSApiIam)
160
+
161
+ def assume_role(self, account_id: str, role: str) -> AWSApi:
162
+ """Return a new AWSApi with the assumed role."""
163
+ credentials = self.sts.assume_role(account_id=account_id, role=role)
164
+ return AWSApi(
165
+ AWSTemporaryCredentials(
166
+ access_key_id=credentials.access_key_id,
167
+ secret_access_key=credentials.secret_access_key,
168
+ session_token=credentials.session_token,
169
+ region=self.session.region_name,
170
+ )
171
+ )
172
+
173
+ def temporary_session(self, duration_seconds: int = 900) -> AWSApi:
174
+ """Return a new AWSAPI with temporary AWS credentials from a session.
175
+
176
+ This is similar to assuming a role, in the sense that the credentials will expire after a certain amount of time.
177
+
178
+ These temporary credentials have the same permissions as the session they were built from, except:
179
+ - they can't be used for anything IAM related
180
+ - for the STS API only the AssumeRole and GetSessionToken actions are allowed
181
+ """
182
+ tmp_creds = self.sts.get_session_token(duration_seconds=duration_seconds)
183
+ return AWSApi(
184
+ AWSTemporaryCredentials(
185
+ access_key_id=tmp_creds.access_key_id,
186
+ secret_access_key=tmp_creds.secret_access_key,
187
+ session_token=tmp_creds.session_token,
188
+ region=self.session.region_name,
189
+ )
190
+ )
@@ -0,0 +1,50 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ if TYPE_CHECKING:
6
+ from mypy_boto3_iam import IAMClient
7
+ else:
8
+ IAMClient = object
9
+
10
+
11
+ class AWSAccessKey(BaseModel):
12
+ access_key_id: str = Field(..., alias="AccessKeyId")
13
+ secret_access_key: str = Field(..., alias="SecretAccessKey")
14
+
15
+
16
+ class AWSUser(BaseModel):
17
+ user_name: str = Field(..., alias="UserName")
18
+ user_id: str = Field(..., alias="UserId")
19
+ arn: str = Field(..., alias="Arn")
20
+ path: str = Field(..., alias="Path")
21
+
22
+
23
+ class AWSApiIam:
24
+ def __init__(self, client: IAMClient) -> None:
25
+ self.client = client
26
+
27
+ def create_access_key(self, user_name: str) -> AWSAccessKey:
28
+ """Create an access key for a given user."""
29
+ credentials = self.client.create_access_key(
30
+ UserName=user_name,
31
+ )
32
+ return AWSAccessKey(**credentials["AccessKey"])
33
+
34
+ def create_user(self, user_name: str) -> AWSUser:
35
+ """Create a new IAM user."""
36
+ user = self.client.create_user(
37
+ UserName=user_name,
38
+ )
39
+ return AWSUser(**user["User"])
40
+
41
+ def attach_user_policy(self, user_name: str, policy_arn: str) -> None:
42
+ """Attach a policy to a user."""
43
+ self.client.attach_user_policy(
44
+ UserName=user_name,
45
+ PolicyArn=policy_arn,
46
+ )
47
+
48
+ def create_account_alias(self, account_alias: str) -> None:
49
+ """Create an account alias."""
50
+ self.client.create_account_alias(AccountAlias=account_alias)
@@ -0,0 +1,104 @@
1
+ from collections.abc import Mapping
2
+ from typing import TYPE_CHECKING
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ if TYPE_CHECKING:
7
+ from mypy_boto3_organizations import OrganizationsClient
8
+ from mypy_boto3_organizations.literals import CreateAccountFailureReasonType
9
+ else:
10
+ OrganizationsClient = object
11
+ CreateAccountFailureReasonType = object
12
+
13
+
14
+ class AwsOrganizationOU(BaseModel):
15
+ id: str = Field(..., alias="Id")
16
+ arn: str = Field(..., alias="Arn")
17
+ name: str = Field(..., alias="Name")
18
+ children: list["AwsOrganizationOU"] = []
19
+
20
+ def find(self, path: str) -> "AwsOrganizationOU":
21
+ """Return an organizational unit by its path."""
22
+ name, *rest = path.strip("/").split("/")
23
+ subs = "/".join(rest)
24
+ if self.name == name:
25
+ if not rest:
26
+ return self
27
+ for child in self.children:
28
+ try:
29
+ return child.find(subs)
30
+ except KeyError:
31
+ pass
32
+ raise KeyError(f"OU not found: {path}")
33
+
34
+
35
+ class AWSAccountStatus(BaseModel):
36
+ id: str = Field(..., alias="Id")
37
+ account_name: str = Field(..., alias="AccountName")
38
+ account_id: str = Field(..., alias="AccountId")
39
+ state: str = Field(..., alias="State")
40
+ failure_reason: CreateAccountFailureReasonType | None = Field(alias="FailureReason")
41
+
42
+
43
+ class AWSAccountCreationException(Exception):
44
+ pass
45
+
46
+
47
+ class AWSApiOrganizations:
48
+ def __init__(self, client: OrganizationsClient) -> None:
49
+ self.client = client
50
+
51
+ def get_organizational_units_tree(
52
+ self, root: AwsOrganizationOU | None = None
53
+ ) -> AwsOrganizationOU:
54
+ """List all organizational units for a given root recursively."""
55
+ if not root:
56
+ root = AwsOrganizationOU(**self.client.list_roots()["Roots"][0])
57
+
58
+ paginator = self.client.get_paginator("list_organizational_units_for_parent")
59
+ for page in paginator.paginate(ParentId=root.id):
60
+ for ou_raw in page["OrganizationalUnits"]:
61
+ ou = AwsOrganizationOU(**ou_raw)
62
+ root.children.append(ou)
63
+ self.get_organizational_units_tree(root=ou)
64
+ return root
65
+
66
+ def create_account(
67
+ self,
68
+ email: str,
69
+ account_name: str,
70
+ tags: Mapping[str, str],
71
+ access_to_billing: bool = True,
72
+ ) -> AWSAccountStatus:
73
+ """Create a new account in the organization."""
74
+ resp = self.client.create_account(
75
+ Email=email,
76
+ AccountName=account_name,
77
+ IamUserAccessToBilling="ALLOW" if access_to_billing else "DENY",
78
+ Tags=[{"Key": k, "Value": v} for k, v in tags.items()],
79
+ )
80
+ status = AWSAccountStatus(**resp["CreateAccountStatus"])
81
+ if status.state == "FAILED":
82
+ raise AWSAccountCreationException(
83
+ f"Account creation failed: {status.failure_reason}"
84
+ )
85
+ return status
86
+
87
+ def describe_create_account_status(
88
+ self, create_account_request_id: str
89
+ ) -> AWSAccountStatus:
90
+ """Return the status of a create account request."""
91
+ resp = self.client.describe_create_account_status(
92
+ CreateAccountRequestId=create_account_request_id
93
+ )
94
+ return AWSAccountStatus(**resp["CreateAccountStatus"])
95
+
96
+ def move_account(
97
+ self, account_id: str, source_parent_id: str, destination_parent_id: str
98
+ ) -> None:
99
+ """Move an account to a different organizational unit."""
100
+ self.client.move_account(
101
+ AccountId=account_id,
102
+ SourceParentId=source_parent_id,
103
+ DestinationParentId=destination_parent_id,
104
+ )
@@ -0,0 +1,36 @@
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ if TYPE_CHECKING:
7
+ from mypy_boto3_sts import STSClient
8
+ else:
9
+ STSClient = object
10
+
11
+
12
+ class AWSCredentials(BaseModel):
13
+ access_key_id: str = Field(..., alias="AccessKeyId")
14
+ secret_access_key: str = Field(..., alias="SecretAccessKey")
15
+ session_token: str = Field(..., alias="SessionToken")
16
+ expiration: datetime = Field(..., alias="Expiration")
17
+
18
+
19
+ class AWSApiSts:
20
+ def __init__(self, client: STSClient) -> None:
21
+ self.client = client
22
+
23
+ def assume_role(self, account_id: str, role: str) -> AWSCredentials:
24
+ """Assume a role and return temporary credentials."""
25
+ assumed_role_object = self.client.assume_role(
26
+ RoleArn=f"arn:aws:iam::{account_id}:role/{role}",
27
+ RoleSessionName=role,
28
+ )
29
+ return AWSCredentials(**assumed_role_object["Credentials"])
30
+
31
+ def get_session_token(self, duration_seconds: int = 900) -> AWSCredentials:
32
+ """Return temporary credentials."""
33
+ assumed_role_object = self.client.get_session_token(
34
+ DurationSeconds=duration_seconds
35
+ )
36
+ return AWSCredentials(**assumed_role_object["Credentials"])