providers.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. # -*- coding: utf-8 -*-
  2. # MinIO Python Library for Amazon S3 Compatible Cloud Storage,
  3. # (C) 2020 MinIO, Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. # pylint: disable=too-many-branches
  17. """Credential providers."""
  18. from __future__ import annotations
  19. import configparser
  20. import ipaddress
  21. import json
  22. import os
  23. import re
  24. import socket
  25. import sys
  26. import time
  27. from abc import ABC, abstractmethod
  28. from datetime import timedelta
  29. from pathlib import Path
  30. from typing import Callable, Optional, cast
  31. from urllib.parse import urlencode, urlsplit, urlunsplit
  32. from xml.etree import ElementTree as ET
  33. import certifi
  34. from urllib3.poolmanager import PoolManager
  35. try:
  36. from urllib3.response import BaseHTTPResponse # type: ignore[attr-defined]
  37. except ImportError:
  38. from urllib3.response import HTTPResponse as BaseHTTPResponse
  39. from urllib3.util import Retry, parse_url
  40. from minio.helpers import sha256_hash, url_replace
  41. from minio.signer import sign_v4_sts
  42. from minio.time import from_iso8601utc, to_amz_date, utcnow
  43. from minio.xml import find, findtext
  44. from .credentials import Credentials
  45. _MIN_DURATION_SECONDS = int(timedelta(minutes=15).total_seconds())
  46. _MAX_DURATION_SECONDS = int(timedelta(days=7).total_seconds())
  47. _DEFAULT_DURATION_SECONDS = int(timedelta(hours=1).total_seconds())
  48. def _parse_credentials(data: str, name: str) -> Credentials:
  49. """Parse data containing credentials XML."""
  50. element = ET.fromstring(data)
  51. element = cast(ET.Element, find(element, name, True))
  52. element = cast(ET.Element, find(element, "Credentials", True))
  53. expiration = from_iso8601utc(findtext(element, "Expiration", True))
  54. return Credentials(
  55. access_key=cast(str, findtext(element, "AccessKeyId", True)),
  56. secret_key=cast(str, findtext(element, "SecretAccessKey", True)),
  57. session_token=findtext(element, "SessionToken", True),
  58. expiration=expiration,
  59. )
  60. def _urlopen(
  61. http_client: PoolManager,
  62. method: str,
  63. url: str,
  64. body: Optional[str | bytes] = None,
  65. headers: Optional[dict[str, str | list[str] | tuple[str]]] = None,
  66. ) -> BaseHTTPResponse:
  67. """Wrapper of urlopen() handles HTTP status code."""
  68. res = http_client.urlopen(method, url, body=body, headers=headers)
  69. if res.status not in [200, 204, 206]:
  70. safe_url = re.sub(
  71. r"LDAPPassword=([^&]+)", "LDAPPassword=*REDACTED*", url,
  72. )
  73. raise ValueError(
  74. f"{safe_url} failed with HTTP status code {res.status}",
  75. )
  76. return res
  77. def _user_home_dir() -> str:
  78. """Return current user home folder."""
  79. return (
  80. os.environ.get("HOME") or
  81. os.environ.get("UserProfile") or
  82. str(Path.home())
  83. )
  84. class Provider(ABC): # pylint: disable=too-few-public-methods
  85. """Credential retriever."""
  86. @abstractmethod
  87. def retrieve(self) -> Credentials:
  88. """Retrieve credentials and its expiry if available."""
  89. class AssumeRoleProvider(Provider):
  90. """Assume-role credential provider."""
  91. def __init__(
  92. self,
  93. *,
  94. sts_endpoint: str,
  95. access_key: str,
  96. secret_key: str,
  97. duration_seconds: int = 0,
  98. policy: Optional[str] = None,
  99. region: Optional[str] = None,
  100. role_arn: Optional[str] = None,
  101. role_session_name: Optional[str] = None,
  102. external_id: Optional[str] = None,
  103. http_client: Optional[PoolManager] = None,
  104. ):
  105. self._sts_endpoint = sts_endpoint
  106. self._access_key = access_key
  107. self._secret_key = secret_key
  108. self._region = region or ""
  109. self._http_client = http_client or PoolManager(
  110. retries=Retry(
  111. total=5,
  112. backoff_factor=0.2,
  113. status_forcelist=[500, 502, 503, 504],
  114. ),
  115. )
  116. query_params = {
  117. "Action": "AssumeRole",
  118. "Version": "2011-06-15",
  119. "DurationSeconds": str(
  120. duration_seconds
  121. if duration_seconds > _DEFAULT_DURATION_SECONDS
  122. else _DEFAULT_DURATION_SECONDS
  123. ),
  124. }
  125. if role_arn:
  126. query_params["RoleArn"] = role_arn
  127. if role_session_name:
  128. query_params["RoleSessionName"] = role_session_name
  129. if policy:
  130. query_params["Policy"] = policy
  131. if external_id:
  132. query_params["ExternalId"] = external_id
  133. self._body = urlencode(query_params)
  134. self._content_sha256 = sha256_hash(self._body)
  135. url = urlsplit(sts_endpoint)
  136. self._url = url
  137. self._host = url.netloc
  138. if (
  139. (url.scheme == "http" and url.port == 80) or
  140. (url.scheme == "https" and url.port == 443)
  141. ):
  142. self._host = cast(str, url.hostname)
  143. self._credentials: Optional[Credentials] = None
  144. def retrieve(self) -> Credentials:
  145. """Retrieve credentials."""
  146. if self._credentials and not self._credentials.is_expired():
  147. return self._credentials
  148. utctime = utcnow()
  149. headers = sign_v4_sts(
  150. method="POST",
  151. url=self._url,
  152. region=self._region,
  153. headers={
  154. "Content-Type": "application/x-www-form-urlencoded",
  155. "Host": self._host,
  156. "X-Amz-Date": to_amz_date(utctime),
  157. },
  158. credentials=Credentials(
  159. access_key=self._access_key,
  160. secret_key=self._secret_key,
  161. ),
  162. content_sha256=self._content_sha256,
  163. date=utctime,
  164. )
  165. res = _urlopen(
  166. self._http_client,
  167. "POST",
  168. self._sts_endpoint,
  169. body=self._body,
  170. headers=headers,
  171. )
  172. self._credentials = _parse_credentials(
  173. res.data.decode(), "AssumeRoleResult",
  174. )
  175. return self._credentials
  176. class ChainedProvider(Provider):
  177. """Chained credential provider."""
  178. def __init__(self, providers: list[Provider]):
  179. self._providers = providers
  180. self._provider: Optional[Provider] = None
  181. self._credentials: Optional[Credentials] = None
  182. def retrieve(self) -> Credentials:
  183. """Retrieve credentials from one of available provider."""
  184. if self._credentials and not self._credentials.is_expired():
  185. return self._credentials
  186. if self._provider:
  187. try:
  188. self._credentials = self._provider.retrieve()
  189. return self._credentials
  190. except ValueError:
  191. # Ignore this error and iterate other providers.
  192. pass
  193. for provider in self._providers:
  194. try:
  195. self._credentials = provider.retrieve()
  196. self._provider = provider
  197. return self._credentials
  198. except ValueError:
  199. # Ignore this error and iterate other providers.
  200. pass
  201. raise ValueError("All providers fail to fetch credentials")
  202. class EnvAWSProvider(Provider):
  203. """Credential provider from AWS environment variables."""
  204. def retrieve(self) -> Credentials:
  205. """Retrieve credentials."""
  206. return Credentials(
  207. access_key=(
  208. cast(
  209. str,
  210. os.environ.get("AWS_ACCESS_KEY_ID") or
  211. os.environ.get("AWS_ACCESS_KEY"),
  212. )
  213. ),
  214. secret_key=(
  215. cast(
  216. str,
  217. os.environ.get("AWS_SECRET_ACCESS_KEY") or
  218. os.environ.get("AWS_SECRET_KEY"),
  219. )
  220. ),
  221. session_token=os.environ.get("AWS_SESSION_TOKEN"),
  222. )
  223. class EnvMinioProvider(Provider):
  224. """Credential provider from MinIO environment variables."""
  225. def retrieve(self) -> Credentials:
  226. """Retrieve credentials."""
  227. return Credentials(
  228. access_key=os.environ.get("MINIO_ACCESS_KEY") or "",
  229. secret_key=os.environ.get("MINIO_SECRET_KEY") or "",
  230. )
  231. class AWSConfigProvider(Provider):
  232. """Credential provider from AWS credential file."""
  233. def __init__(
  234. self,
  235. filename: Optional[str] = None,
  236. profile: Optional[str] = None,
  237. ):
  238. self._filename = (
  239. filename or
  240. os.environ.get("AWS_SHARED_CREDENTIALS_FILE") or
  241. os.path.join(_user_home_dir(), ".aws", "credentials")
  242. )
  243. self._profile = profile or os.environ.get("AWS_PROFILE") or "default"
  244. def retrieve(self) -> Credentials:
  245. """Retrieve credentials from AWS configuration file."""
  246. parser = configparser.ConfigParser()
  247. parser.read(self._filename)
  248. access_key = parser.get(
  249. self._profile,
  250. "aws_access_key_id",
  251. fallback=None,
  252. )
  253. secret_key = parser.get(
  254. self._profile,
  255. "aws_secret_access_key",
  256. fallback=None,
  257. )
  258. session_token = parser.get(
  259. self._profile,
  260. "aws_session_token",
  261. fallback=None,
  262. )
  263. if not access_key:
  264. raise ValueError(
  265. f"access key does not exist in profile "
  266. f"{self._profile} in AWS credential file {self._filename}"
  267. )
  268. if not secret_key:
  269. raise ValueError(
  270. f"secret key does not exist in profile "
  271. f"{self._profile} in AWS credential file {self._filename}"
  272. )
  273. return Credentials(
  274. access_key,
  275. secret_key,
  276. session_token=session_token,
  277. )
  278. class MinioClientConfigProvider(Provider):
  279. """Credential provider from MinIO Client configuration file."""
  280. def __init__(
  281. self,
  282. filename: Optional[str] = None,
  283. alias: Optional[str] = None,
  284. ):
  285. self._filename = (
  286. filename or
  287. os.path.join(
  288. _user_home_dir(),
  289. "mc" if sys.platform == "win32" else ".mc",
  290. "config.json",
  291. )
  292. )
  293. self._alias = alias or os.environ.get("MINIO_ALIAS") or "s3"
  294. def retrieve(self) -> Credentials:
  295. """Retrieve credential value from MinIO client configuration file."""
  296. try:
  297. with open(self._filename, encoding="utf-8") as conf_file:
  298. config = json.load(conf_file)
  299. aliases = config.get("hosts") or config.get("aliases")
  300. if not aliases:
  301. raise ValueError(
  302. f"invalid configuration in file {self._filename}",
  303. )
  304. creds = aliases.get(self._alias)
  305. if not creds:
  306. raise ValueError(
  307. f"alias {self._alias} not found in MinIO client"
  308. f"configuration file {self._filename}"
  309. )
  310. return Credentials(creds.get("accessKey"), creds.get("secretKey"))
  311. except (IOError, OSError) as exc:
  312. raise ValueError(
  313. f"error in reading file {self._filename}",
  314. ) from exc
  315. def _check_loopback_host(url: str):
  316. """Check whether host in url points only to localhost."""
  317. host = cast(str, parse_url(url).host)
  318. try:
  319. addrs = set(info[4][0] for info in socket.getaddrinfo(host, None))
  320. for addr in addrs:
  321. if not ipaddress.ip_address(addr).is_loopback:
  322. raise ValueError(host + " is not loopback only host")
  323. except socket.gaierror as exc:
  324. raise ValueError("Host " + host + " is not loopback address") from exc
  325. def _get_jwt_token(token_file: str) -> dict[str, str]:
  326. """Read and return content of token file. """
  327. try:
  328. with open(token_file, encoding="utf-8") as file:
  329. return {"access_token": file.read(), "expires_in": "0"}
  330. except (IOError, OSError) as exc:
  331. raise ValueError(f"error in reading file {token_file}") from exc
  332. class IamAwsProvider(Provider):
  333. """Credential provider using IAM roles for Amazon EC2/ECS."""
  334. def __init__(
  335. self,
  336. *,
  337. custom_endpoint: Optional[str] = None,
  338. http_client: Optional[PoolManager] = None,
  339. auth_token: Optional[str] = None,
  340. relative_uri: Optional[str] = None,
  341. full_uri: Optional[str] = None,
  342. token_file: Optional[str] = None,
  343. role_arn: Optional[str] = None,
  344. role_session_name: Optional[str] = None,
  345. region: Optional[str] = None,
  346. ):
  347. self._custom_endpoint = custom_endpoint
  348. self._http_client = http_client or PoolManager(
  349. retries=Retry(
  350. total=5,
  351. backoff_factor=0.2,
  352. status_forcelist=[500, 502, 503, 504],
  353. ),
  354. )
  355. self._token = (
  356. os.environ.get("AWS_CONTAINER_AUTHORIZATION_TOKEN") or
  357. auth_token
  358. )
  359. self._token_file = (
  360. os.environ.get("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE") or
  361. auth_token
  362. )
  363. self._identity_file = (
  364. os.environ.get("AWS_WEB_IDENTITY_TOKEN_FILE") or token_file
  365. )
  366. self._aws_region = os.environ.get("AWS_REGION") or region
  367. self._role_arn = os.environ.get("AWS_ROLE_ARN") or role_arn
  368. self._role_session_name = (
  369. os.environ.get("AWS_ROLE_SESSION_NAME") or role_session_name
  370. )
  371. self._relative_uri = (
  372. os.environ.get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") or
  373. relative_uri
  374. )
  375. if self._relative_uri and not self._relative_uri.startswith("/"):
  376. self._relative_uri = "/" + self._relative_uri
  377. self._full_uri = (
  378. os.environ.get("AWS_CONTAINER_CREDENTIALS_FULL_URI") or
  379. full_uri
  380. )
  381. self._credentials: Optional[Credentials] = None
  382. def fetch(
  383. self,
  384. url: str,
  385. headers: Optional[dict[str, str | list[str] | tuple[str]]] = None,
  386. ) -> Credentials:
  387. """Fetch credentials from EC2/ECS."""
  388. res = _urlopen(self._http_client, "GET", url, headers=headers)
  389. data = json.loads(res.data)
  390. if data.get("Code", "Success") != "Success":
  391. raise ValueError(
  392. f"{url} failed with code {data['Code']} "
  393. f"message {data.get('Message')}"
  394. )
  395. data["Expiration"] = from_iso8601utc(data["Expiration"])
  396. return Credentials(
  397. data["AccessKeyId"],
  398. data["SecretAccessKey"],
  399. data["Token"],
  400. data["Expiration"],
  401. )
  402. def retrieve(self) -> Credentials:
  403. """Retrieve credentials from WebIdentity/EC2/ECS."""
  404. if self._credentials and not self._credentials.is_expired():
  405. return self._credentials
  406. url = self._custom_endpoint
  407. if self._identity_file:
  408. if not url:
  409. url = "https://sts.amazonaws.com"
  410. if self._aws_region:
  411. url = f"https://sts.{self._aws_region}.amazonaws.com"
  412. if self._aws_region.startswith("cn-"):
  413. url += ".cn"
  414. provider = WebIdentityProvider(
  415. jwt_provider_func=lambda: _get_jwt_token(
  416. cast(str, self._identity_file),
  417. ),
  418. sts_endpoint=url,
  419. role_arn=self._role_arn,
  420. role_session_name=self._role_session_name,
  421. http_client=self._http_client,
  422. )
  423. self._credentials = provider.retrieve()
  424. return cast(Credentials, self._credentials)
  425. headers: Optional[dict[str, str | list[str] | tuple[str]]] = None
  426. if self._relative_uri:
  427. if not url:
  428. url = "http://169.254.170.2" + self._relative_uri
  429. headers = {"Authorization": self._token} if self._token else None
  430. elif self._full_uri:
  431. token = self._token
  432. if self._token_file:
  433. url = self._full_uri
  434. with open(self._token_file, encoding="utf-8") as file:
  435. token = file.read()
  436. else:
  437. if not url:
  438. url = self._full_uri
  439. _check_loopback_host(url)
  440. headers = {"Authorization": token} if token else None
  441. else:
  442. if not url:
  443. url = "http://169.254.169.254"
  444. # Get IMDS Token
  445. res = _urlopen(
  446. self._http_client,
  447. "PUT",
  448. url+"/latest/api/token",
  449. headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
  450. )
  451. token = res.data.decode("utf-8")
  452. headers = {"X-aws-ec2-metadata-token": token} if token else None
  453. # Get role name
  454. url = urlunsplit(
  455. url_replace(
  456. url=urlsplit(url),
  457. path="/latest/meta-data/iam/security-credentials/",
  458. ),
  459. )
  460. res = _urlopen(self._http_client, "GET", url, headers=headers)
  461. role_names = res.data.decode("utf-8").split("\n")
  462. if not role_names:
  463. raise ValueError(f"no IAM roles attached to EC2 service {url}")
  464. url += role_names[0].strip("\r")
  465. if not url:
  466. raise ValueError("url is empty; this should not happen")
  467. self._credentials = self.fetch(url, headers=headers)
  468. return self._credentials
  469. class LdapIdentityProvider(Provider):
  470. """Credential provider using AssumeRoleWithLDAPIdentity API."""
  471. def __init__( # pylint: disable=too-many-positional-arguments
  472. self,
  473. sts_endpoint: str,
  474. ldap_username: str,
  475. ldap_password: str,
  476. duration_seconds: Optional[int] = None,
  477. policy: Optional[str] = None,
  478. token_revoke_type: Optional[str] = None,
  479. http_client: Optional[PoolManager] = None,
  480. ):
  481. query_params = {
  482. "Action": "AssumeRoleWithLDAPIdentity",
  483. "Version": "2011-06-15",
  484. "LDAPUsername": ldap_username,
  485. "LDAPPassword": ldap_password,
  486. }
  487. if duration_seconds:
  488. query_params["DurationSeconds"] = str(duration_seconds)
  489. if policy:
  490. query_params["Policy"] = policy
  491. if token_revoke_type:
  492. query_params["TokenRevokeType"] = token_revoke_type
  493. self._sts_endpoint = sts_endpoint + "?" + urlencode(query_params)
  494. self._http_client = http_client or PoolManager(
  495. retries=Retry(
  496. total=5,
  497. backoff_factor=0.2,
  498. status_forcelist=[500, 502, 503, 504],
  499. ),
  500. )
  501. self._credentials: Optional[Credentials] = None
  502. def retrieve(self) -> Credentials:
  503. """Retrieve credentials."""
  504. if self._credentials and not self._credentials.is_expired():
  505. return self._credentials
  506. res = _urlopen(
  507. self._http_client,
  508. "POST",
  509. self._sts_endpoint,
  510. )
  511. self._credentials = _parse_credentials(
  512. res.data.decode(), "AssumeRoleWithLDAPIdentityResult",
  513. )
  514. return self._credentials
  515. class StaticProvider(Provider):
  516. """Fixed credential provider."""
  517. def __init__(
  518. self,
  519. access_key: str,
  520. secret_key: str,
  521. session_token: Optional[str] = None,
  522. ):
  523. self._credentials = Credentials(access_key, secret_key, session_token)
  524. def retrieve(self) -> Credentials:
  525. """Return passed credentials."""
  526. return self._credentials
  527. class WebIdentityClientGrantsProvider(Provider, ABC):
  528. """Base class for WebIdentity and ClientGrants credentials provider."""
  529. def __init__(
  530. self,
  531. *,
  532. jwt_provider_func: Callable[[], dict[str, str]],
  533. sts_endpoint: str,
  534. duration_seconds: int = 0,
  535. policy: Optional[str] = None,
  536. role_arn: Optional[str] = None,
  537. role_session_name: Optional[str] = None,
  538. http_client: Optional[PoolManager] = None,
  539. ):
  540. self._jwt_provider_func = jwt_provider_func
  541. self._sts_endpoint = sts_endpoint
  542. self._duration_seconds = duration_seconds
  543. self._policy = policy
  544. self._role_arn = role_arn
  545. self._role_session_name = role_session_name
  546. self._http_client = http_client or PoolManager(
  547. retries=Retry(
  548. total=5,
  549. backoff_factor=0.2,
  550. status_forcelist=[500, 502, 503, 504],
  551. ),
  552. )
  553. self._credentials: Optional[Credentials] = None
  554. @abstractmethod
  555. def _is_web_identity(self) -> bool:
  556. """Check if derived class deal with WebIdentity."""
  557. def _get_duration_seconds(self, expiry: int) -> int:
  558. """Get DurationSeconds optimal value."""
  559. if self._duration_seconds:
  560. expiry = self._duration_seconds
  561. if expiry > _MAX_DURATION_SECONDS:
  562. return _MAX_DURATION_SECONDS
  563. if expiry <= 0:
  564. return expiry
  565. return (
  566. _MIN_DURATION_SECONDS if expiry < _MIN_DURATION_SECONDS else expiry
  567. )
  568. def retrieve(self) -> Credentials:
  569. """Retrieve credentials."""
  570. if self._credentials and not self._credentials.is_expired():
  571. return self._credentials
  572. jwt = self._jwt_provider_func()
  573. query_params = {"Version": "2011-06-15"}
  574. duration_seconds = self._get_duration_seconds(
  575. int(jwt.get("expires_in", "0")),
  576. )
  577. if duration_seconds:
  578. query_params["DurationSeconds"] = str(duration_seconds)
  579. if self._policy:
  580. query_params["Policy"] = self._policy
  581. access_token = jwt.get("access_token") or jwt.get("id_token", "")
  582. if self._is_web_identity():
  583. query_params["Action"] = "AssumeRoleWithWebIdentity"
  584. query_params["WebIdentityToken"] = access_token
  585. if self._role_arn:
  586. query_params["RoleArn"] = self._role_arn
  587. query_params["RoleSessionName"] = (
  588. self._role_session_name
  589. if self._role_session_name
  590. else str(time.time()).replace(".", "")
  591. )
  592. else:
  593. query_params["Action"] = "AssumeRoleWithClientGrants"
  594. query_params["Token"] = access_token
  595. url = self._sts_endpoint + "?" + urlencode(query_params)
  596. res = _urlopen(self._http_client, "POST", url)
  597. self._credentials = _parse_credentials(
  598. res.data.decode(),
  599. (
  600. "AssumeRoleWithWebIdentityResult"
  601. if self._is_web_identity()
  602. else "AssumeRoleWithClientGrantsResult"
  603. ),
  604. )
  605. return self._credentials
  606. class ClientGrantsProvider(WebIdentityClientGrantsProvider):
  607. """Credential provider using AssumeRoleWithClientGrants API."""
  608. def __init__(
  609. self,
  610. *,
  611. jwt_provider_func: Callable[[], dict[str, str]],
  612. sts_endpoint: str,
  613. duration_seconds: int = 0,
  614. policy: Optional[str] = None,
  615. http_client: Optional[PoolManager] = None,
  616. ):
  617. super().__init__(
  618. jwt_provider_func=jwt_provider_func,
  619. sts_endpoint=sts_endpoint,
  620. duration_seconds=duration_seconds,
  621. policy=policy,
  622. http_client=http_client,
  623. )
  624. def _is_web_identity(self) -> bool:
  625. return False
  626. class WebIdentityProvider(WebIdentityClientGrantsProvider):
  627. """Credential provider using AssumeRoleWithWebIdentity API."""
  628. def _is_web_identity(self) -> bool:
  629. return True
  630. class CertificateIdentityProvider(Provider):
  631. """Credential provider using AssumeRoleWithCertificate API."""
  632. def __init__(
  633. self,
  634. *,
  635. sts_endpoint: str,
  636. cert_file: Optional[str] = None,
  637. key_file: Optional[str] = None,
  638. key_password: Optional[str] = None,
  639. ca_certs: Optional[str] = None,
  640. duration_seconds: int = 0,
  641. http_client: Optional[PoolManager] = None,
  642. ):
  643. if urlsplit(sts_endpoint).scheme != "https":
  644. raise ValueError("STS endpoint scheme must be HTTPS")
  645. if not bool(http_client) != (cert_file and key_file):
  646. raise ValueError(
  647. "either cert/key file or custom http_client must be provided",
  648. )
  649. self._sts_endpoint = sts_endpoint + "?" + urlencode(
  650. {
  651. "Action": "AssumeRoleWithCertificate",
  652. "Version": "2011-06-15",
  653. "DurationSeconds": str(
  654. duration_seconds
  655. if duration_seconds > _DEFAULT_DURATION_SECONDS
  656. else _DEFAULT_DURATION_SECONDS
  657. ),
  658. },
  659. )
  660. self._http_client = http_client or PoolManager(
  661. maxsize=10,
  662. cert_file=cert_file,
  663. cert_reqs='CERT_REQUIRED',
  664. key_file=key_file,
  665. key_password=key_password,
  666. ca_certs=ca_certs or certifi.where(),
  667. retries=Retry(
  668. total=5,
  669. backoff_factor=0.2,
  670. status_forcelist=[500, 502, 503, 504],
  671. ),
  672. )
  673. self._credentials: Optional[Credentials] = None
  674. def retrieve(self) -> Credentials:
  675. """Retrieve credentials."""
  676. if self._credentials and not self._credentials.is_expired():
  677. return self._credentials
  678. res = _urlopen(
  679. self._http_client,
  680. "POST",
  681. self._sts_endpoint,
  682. )
  683. self._credentials = _parse_credentials(
  684. res.data.decode(), "AssumeRoleWithCertificateResult",
  685. )
  686. return self._credentials