apache-airflow-providers-amazon 9.5.0rc1__py3-none-any.whl → 9.5.0rc2__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.
Files changed (27) hide show
  1. airflow/providers/amazon/aws/auth_manager/avp/entities.py +2 -0
  2. airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py +56 -5
  3. airflow/providers/amazon/aws/auth_manager/router/login.py +8 -4
  4. airflow/providers/amazon/aws/hooks/appflow.py +5 -15
  5. airflow/providers/amazon/aws/hooks/base_aws.py +9 -1
  6. airflow/providers/amazon/aws/hooks/eks.py +3 -6
  7. airflow/providers/amazon/aws/hooks/s3.py +3 -1
  8. airflow/providers/amazon/aws/links/base_aws.py +2 -1
  9. airflow/providers/amazon/aws/notifications/chime.py +1 -2
  10. airflow/providers/amazon/aws/notifications/sns.py +1 -1
  11. airflow/providers/amazon/aws/notifications/sqs.py +1 -1
  12. airflow/providers/amazon/aws/operators/ec2.py +91 -83
  13. airflow/providers/amazon/aws/operators/mwaa.py +73 -2
  14. airflow/providers/amazon/aws/operators/sagemaker.py +1 -2
  15. airflow/providers/amazon/aws/sensors/ec2.py +5 -12
  16. airflow/providers/amazon/aws/sensors/mwaa.py +58 -11
  17. airflow/providers/amazon/aws/transfers/redshift_to_s3.py +19 -4
  18. airflow/providers/amazon/aws/transfers/s3_to_redshift.py +19 -3
  19. airflow/providers/amazon/aws/triggers/base.py +10 -1
  20. airflow/providers/amazon/aws/triggers/mwaa.py +128 -0
  21. airflow/providers/amazon/aws/utils/waiter_with_logging.py +4 -3
  22. airflow/providers/amazon/aws/waiters/mwaa.json +36 -0
  23. airflow/providers/amazon/get_provider_info.py +9 -3
  24. {apache_airflow_providers_amazon-9.5.0rc1.dist-info → apache_airflow_providers_amazon-9.5.0rc2.dist-info}/METADATA +9 -7
  25. {apache_airflow_providers_amazon-9.5.0rc1.dist-info → apache_airflow_providers_amazon-9.5.0rc2.dist-info}/RECORD +27 -25
  26. {apache_airflow_providers_amazon-9.5.0rc1.dist-info → apache_airflow_providers_amazon-9.5.0rc2.dist-info}/WHEEL +0 -0
  27. {apache_airflow_providers_amazon-9.5.0rc1.dist-info → apache_airflow_providers_amazon-9.5.0rc2.dist-info}/entry_points.txt +0 -0
@@ -34,6 +34,8 @@ class AvpEntities(Enum):
34
34
 
35
35
  # Resource types
36
36
  ASSET = "Asset"
37
+ ASSET_ALIAS = "AssetAlias"
38
+ BACKFILL = "Backfills"
37
39
  CONFIGURATION = "Configuration"
38
40
  CONNECTION = "Connection"
39
41
  CUSTOM = "Custom"
@@ -21,12 +21,15 @@ from collections import defaultdict
21
21
  from collections.abc import Sequence
22
22
  from functools import cached_property
23
23
  from typing import TYPE_CHECKING, Any, cast
24
+ from urllib.parse import urljoin
24
25
 
25
26
  from fastapi import FastAPI
26
27
 
28
+ from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
27
29
  from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
28
30
  from airflow.api_fastapi.auth.managers.models.resource_details import (
29
31
  AccessView,
32
+ BackfillDetails,
30
33
  ConnectionDetails,
31
34
  DagAccessEntity,
32
35
  DagDetails,
@@ -55,7 +58,12 @@ if TYPE_CHECKING:
55
58
  IsAuthorizedPoolRequest,
56
59
  IsAuthorizedVariableRequest,
57
60
  )
58
- from airflow.api_fastapi.auth.managers.models.resource_details import AssetDetails, ConfigurationDetails
61
+ from airflow.api_fastapi.auth.managers.models.resource_details import (
62
+ AssetAliasDetails,
63
+ AssetDetails,
64
+ ConfigurationDetails,
65
+ )
66
+ from airflow.api_fastapi.common.types import MenuItem
59
67
 
60
68
 
61
69
  class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
@@ -84,11 +92,11 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
84
92
  return conf.get("api", "base_url")
85
93
 
86
94
  def deserialize_user(self, token: dict[str, Any]) -> AwsAuthManagerUser:
87
- return AwsAuthManagerUser(**token)
95
+ return AwsAuthManagerUser(user_id=token.pop("sub"), **token)
88
96
 
89
97
  def serialize_user(self, user: AwsAuthManagerUser) -> dict[str, Any]:
90
98
  return {
91
- "user_id": user.get_id(),
99
+ "sub": user.get_id(),
92
100
  "groups": user.get_groups(),
93
101
  "username": user.username,
94
102
  "email": user.email,
@@ -150,6 +158,14 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
150
158
  context=context,
151
159
  )
152
160
 
161
+ def is_authorized_backfill(
162
+ self, *, method: ResourceMethod, user: AwsAuthManagerUser, details: BackfillDetails | None = None
163
+ ) -> bool:
164
+ backfill_id = details.id if details else None
165
+ return self.avp_facade.is_authorized(
166
+ method=method, entity_type=AvpEntities.BACKFILL, user=user, entity_id=backfill_id
167
+ )
168
+
153
169
  def is_authorized_asset(
154
170
  self, *, method: ResourceMethod, user: AwsAuthManagerUser, details: AssetDetails | None = None
155
171
  ) -> bool:
@@ -158,6 +174,14 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
158
174
  method=method, entity_type=AvpEntities.ASSET, user=user, entity_id=asset_id
159
175
  )
160
176
 
177
+ def is_authorized_asset_alias(
178
+ self, *, method: ResourceMethod, user: AwsAuthManagerUser, details: AssetAliasDetails | None = None
179
+ ) -> bool:
180
+ asset_alias_id = details.id if details else None
181
+ return self.avp_facade.is_authorized(
182
+ method=method, entity_type=AvpEntities.ASSET_ALIAS, user=user, entity_id=asset_alias_id
183
+ )
184
+
161
185
  def is_authorized_pool(
162
186
  self, *, method: ResourceMethod, user: AwsAuthManagerUser, details: PoolDetails | None = None
163
187
  ) -> bool:
@@ -203,6 +227,25 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
203
227
  entity_id=resource_name,
204
228
  )
205
229
 
230
+ def filter_authorized_menu_items(
231
+ self, menu_items: list[MenuItem], *, user: AwsAuthManagerUser
232
+ ) -> list[MenuItem]:
233
+ requests: dict[str, IsAuthorizedRequest] = {}
234
+ for menu_item in menu_items:
235
+ requests[menu_item.value] = self._get_menu_item_request(menu_item.value)
236
+
237
+ batch_is_authorized_results = self.avp_facade.get_batch_is_authorized_results(
238
+ requests=list(requests.values()), user=user
239
+ )
240
+
241
+ def _has_access_to_menu_item(request: IsAuthorizedRequest):
242
+ result = self.avp_facade.get_batch_is_authorized_single_result(
243
+ batch_is_authorized_results=batch_is_authorized_results, request=request, user=user
244
+ )
245
+ return result["decision"] == "ALLOW"
246
+
247
+ return [menu_item for menu_item in menu_items if _has_access_to_menu_item(requests[menu_item.value])]
248
+
206
249
  def batch_is_authorized_connection(
207
250
  self,
208
251
  requests: Sequence[IsAuthorizedConnectionRequest],
@@ -278,7 +321,7 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
278
321
  ]
279
322
  return self.avp_facade.batch_is_authorized(requests=facade_requests, user=user)
280
323
 
281
- def filter_permitted_dag_ids(
324
+ def filter_authorized_dag_ids(
282
325
  self,
283
326
  *,
284
327
  dag_ids: set[str],
@@ -309,7 +352,7 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
309
352
  return {dag_id for dag_id in dag_ids if _has_access_to_dag(requests[dag_id][method])}
310
353
 
311
354
  def get_url_login(self, **kwargs) -> str:
312
- return f"{self.apiserver_endpoint}/auth/login"
355
+ return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login")
313
356
 
314
357
  @staticmethod
315
358
  def get_cli_commands() -> list[CLICommand]:
@@ -337,6 +380,14 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
337
380
 
338
381
  return app
339
382
 
383
+ @staticmethod
384
+ def _get_menu_item_request(menu_item_text: str) -> IsAuthorizedRequest:
385
+ return {
386
+ "method": "MENU",
387
+ "entity_type": AvpEntities.MENU,
388
+ "entity_id": menu_item_text,
389
+ }
390
+
340
391
  def _check_avp_schema_version(self):
341
392
  if not self.avp_facade.is_policy_store_schema_up_to_date():
342
393
  self.log.warning(
@@ -25,7 +25,8 @@ from fastapi import HTTPException, Request
25
25
  from starlette import status
26
26
  from starlette.responses import RedirectResponse
27
27
 
28
- from airflow.api_fastapi.app import get_auth_manager
28
+ from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX, get_auth_manager
29
+ from airflow.api_fastapi.auth.managers.base_auth_manager import COOKIE_NAME_JWT_TOKEN
29
30
  from airflow.api_fastapi.common.router import AirflowRouter
30
31
  from airflow.configuration import conf
31
32
  from airflow.providers.amazon.aws.auth_manager.constants import CONF_SAML_METADATA_URL_KEY, CONF_SECTION_NAME
@@ -79,8 +80,11 @@ def login_callback(request: Request):
79
80
  username=saml_auth.get_nameid(),
80
81
  email=attributes["email"][0] if "email" in attributes else None,
81
82
  )
82
- url = f"{conf.get('api', 'base_url')}/?token={get_auth_manager().get_jwt_token(user)}"
83
- return RedirectResponse(url=url, status_code=303)
83
+ url = conf.get("api", "base_url")
84
+ token = get_auth_manager().generate_jwt(user)
85
+ response = RedirectResponse(url=url, status_code=303)
86
+ response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, secure=True)
87
+ return response
84
88
 
85
89
 
86
90
  def _init_saml_auth(request: Request) -> OneLogin_Saml2_Auth:
@@ -93,7 +97,7 @@ def _init_saml_auth(request: Request) -> OneLogin_Saml2_Auth:
93
97
  "sp": {
94
98
  "entityId": "aws-auth-manager-saml-client",
95
99
  "assertionConsumerService": {
96
- "url": f"{base_url}/auth/login_callback",
100
+ "url": f"{base_url}{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login_callback",
97
101
  "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
98
102
  },
99
103
  },
@@ -16,15 +16,7 @@
16
16
  # under the License.
17
17
  from __future__ import annotations
18
18
 
19
- from collections.abc import Sequence
20
- from typing import TYPE_CHECKING, cast
21
-
22
- from mypy_boto3_appflow.type_defs import (
23
- DestinationFlowConfigTypeDef,
24
- SourceFlowConfigTypeDef,
25
- TaskTypeDef,
26
- TriggerConfigTypeDef,
27
- )
19
+ from typing import TYPE_CHECKING
28
20
 
29
21
  from airflow.providers.amazon.aws.hooks.base_aws import AwsGenericHook
30
22
  from airflow.providers.amazon.aws.utils.waiter_with_logging import wait
@@ -125,11 +117,9 @@ class AppflowHook(AwsGenericHook["AppflowClient"]):
125
117
 
126
118
  self.conn.update_flow(
127
119
  flowName=response["flowName"],
128
- destinationFlowConfigList=cast(
129
- Sequence[DestinationFlowConfigTypeDef], response["destinationFlowConfigList"]
130
- ),
131
- sourceFlowConfig=cast(SourceFlowConfigTypeDef, response["sourceFlowConfig"]),
132
- triggerConfig=cast(TriggerConfigTypeDef, response["triggerConfig"]),
120
+ destinationFlowConfigList=response["destinationFlowConfigList"],
121
+ sourceFlowConfig=response["sourceFlowConfig"],
122
+ triggerConfig=response["triggerConfig"],
133
123
  description=response.get("description", "Flow description."),
134
- tasks=cast(Sequence[TaskTypeDef], tasks),
124
+ tasks=tasks,
135
125
  )
@@ -943,6 +943,7 @@ class AwsGenericHook(BaseHook, Generic[BaseAwsConnection]):
943
943
  self,
944
944
  waiter_name: str,
945
945
  parameters: dict[str, str] | None = None,
946
+ config_overrides: dict[str, Any] | None = None,
946
947
  deferrable: bool = False,
947
948
  client=None,
948
949
  ) -> Waiter:
@@ -962,6 +963,9 @@ class AwsGenericHook(BaseHook, Generic[BaseAwsConnection]):
962
963
  :param parameters: will scan the waiter config for the keys of that dict,
963
964
  and replace them with the corresponding value. If a custom waiter has
964
965
  such keys to be expanded, they need to be provided here.
966
+ Note: cannot be used if parameters are included in config_overrides
967
+ :param config_overrides: will update values of provided keys in the waiter's
968
+ config. Only specified keys will be updated.
965
969
  :param deferrable: If True, the waiter is going to be an async custom waiter.
966
970
  An async client must be provided in that case.
967
971
  :param client: The client to use for the waiter's operations
@@ -970,14 +974,18 @@ class AwsGenericHook(BaseHook, Generic[BaseAwsConnection]):
970
974
 
971
975
  if deferrable and not client:
972
976
  raise ValueError("client must be provided for a deferrable waiter.")
977
+ if parameters is not None and config_overrides is not None and "acceptors" in config_overrides:
978
+ raise ValueError('parameters must be None when "acceptors" is included in config_overrides')
973
979
  # Currently, the custom waiter doesn't work with resource_type, only client_type is supported.
974
980
  client = client or self._client
975
981
  if self.waiter_path and (waiter_name in self._list_custom_waiters()):
976
982
  # Technically if waiter_name is in custom_waiters then self.waiter_path must
977
983
  # exist but MyPy doesn't like the fact that self.waiter_path could be None.
978
984
  with open(self.waiter_path) as config_file:
979
- config = json.loads(config_file.read())
985
+ config: dict = json.loads(config_file.read())
980
986
 
987
+ if config_overrides is not None:
988
+ config["waiters"][waiter_name].update(config_overrides)
981
989
  config = self._apply_parameters_value(config, waiter_name, parameters)
982
990
  return BaseBotoWaiter(client=client, model_config=config, deferrable=deferrable).waiter(
983
991
  waiter_name
@@ -35,7 +35,6 @@ from botocore.signers import RequestSigner
35
35
  from airflow.providers.amazon.aws.hooks.base_aws import AwsBaseHook
36
36
  from airflow.providers.amazon.aws.hooks.sts import StsHook
37
37
  from airflow.utils import yaml
38
- from airflow.utils.json import AirflowJsonEncoder
39
38
 
40
39
  DEFAULT_PAGINATION_TOKEN = ""
41
40
  STS_TOKEN_EXPIRES_IN = 60
@@ -315,7 +314,7 @@ class EksHook(AwsBaseHook):
315
314
  )
316
315
  if verbose:
317
316
  cluster_data = response.get("cluster")
318
- self.log.info("Amazon EKS cluster details: %s", json.dumps(cluster_data, cls=AirflowJsonEncoder))
317
+ self.log.info("Amazon EKS cluster details: %s", json.dumps(cluster_data, default=repr))
319
318
  return response
320
319
 
321
320
  def describe_nodegroup(self, clusterName: str, nodegroupName: str, verbose: bool = False) -> dict:
@@ -343,7 +342,7 @@ class EksHook(AwsBaseHook):
343
342
  nodegroup_data = response.get("nodegroup")
344
343
  self.log.info(
345
344
  "Amazon EKS managed node group details: %s",
346
- json.dumps(nodegroup_data, cls=AirflowJsonEncoder),
345
+ json.dumps(nodegroup_data, default=repr),
347
346
  )
348
347
  return response
349
348
 
@@ -374,9 +373,7 @@ class EksHook(AwsBaseHook):
374
373
  )
375
374
  if verbose:
376
375
  fargate_profile_data = response.get("fargateProfile")
377
- self.log.info(
378
- "AWS Fargate profile details: %s", json.dumps(fargate_profile_data, cls=AirflowJsonEncoder)
379
- )
376
+ self.log.info("AWS Fargate profile details: %s", json.dumps(fargate_profile_data, default=repr))
380
377
  return response
381
378
 
382
379
  def get_cluster_state(self, clusterName: str) -> ClusterStates:
@@ -1494,7 +1494,9 @@ class S3Hook(AwsBaseHook):
1494
1494
  get_hook_lineage_collector().add_output_asset(
1495
1495
  context=self,
1496
1496
  scheme="file",
1497
- asset_kwargs={"path": file_path if file_path.is_absolute() else file_path.absolute()},
1497
+ asset_kwargs={
1498
+ "path": str(file_path) if file_path.is_absolute() else str(file_path.absolute())
1499
+ },
1498
1500
  )
1499
1501
  file = open(file_path, "wb")
1500
1502
  else:
@@ -19,7 +19,6 @@ from __future__ import annotations
19
19
 
20
20
  from typing import TYPE_CHECKING, ClassVar
21
21
 
22
- from airflow.models import XCom
23
22
  from airflow.providers.amazon.aws.utils.suppress import return_on_error
24
23
  from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS
25
24
 
@@ -30,7 +29,9 @@ if TYPE_CHECKING:
30
29
 
31
30
  if AIRFLOW_V_3_0_PLUS:
32
31
  from airflow.sdk import BaseOperatorLink
32
+ from airflow.sdk.execution_time.xcom import XCom
33
33
  else:
34
+ from airflow.models import XCom # type: ignore[no-redef]
34
35
  from airflow.models.baseoperatorlink import BaseOperatorLink # type: ignore[no-redef]
35
36
 
36
37
 
@@ -21,12 +21,11 @@ from functools import cached_property
21
21
  from typing import TYPE_CHECKING
22
22
 
23
23
  from airflow.providers.amazon.aws.hooks.chime import ChimeWebhookHook
24
+ from airflow.providers.common.compat.notifier import BaseNotifier
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  from airflow.utils.context import Context
27
28
 
28
- from airflow.notifications.basenotifier import BaseNotifier
29
-
30
29
 
31
30
  class ChimeNotifier(BaseNotifier):
32
31
  """
@@ -20,8 +20,8 @@ from __future__ import annotations
20
20
  from collections.abc import Sequence
21
21
  from functools import cached_property
22
22
 
23
- from airflow.notifications.basenotifier import BaseNotifier
24
23
  from airflow.providers.amazon.aws.hooks.sns import SnsHook
24
+ from airflow.providers.common.compat.notifier import BaseNotifier
25
25
 
26
26
 
27
27
  class SnsNotifier(BaseNotifier):
@@ -20,8 +20,8 @@ from __future__ import annotations
20
20
  from collections.abc import Sequence
21
21
  from functools import cached_property
22
22
 
23
- from airflow.notifications.basenotifier import BaseNotifier
24
23
  from airflow.providers.amazon.aws.hooks.sqs import SqsHook
24
+ from airflow.providers.common.compat.notifier import BaseNotifier
25
25
 
26
26
 
27
27
  class SqsNotifier(BaseNotifier):