apache-airflow-providers-fab 3.0.0rc1__py3-none-any.whl → 3.1.0rc1__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 (50) hide show
  1. airflow/providers/fab/__init__.py +1 -1
  2. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +2 -2
  3. airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +0 -7
  4. airflow/providers/fab/auth_manager/api_fastapi/datamodels/roles.py +63 -0
  5. airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml +416 -16
  6. airflow/providers/fab/auth_manager/api_fastapi/parameters.py +55 -0
  7. airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +37 -5
  8. airflow/providers/fab/auth_manager/api_fastapi/routes/roles.py +137 -0
  9. airflow/providers/fab/auth_manager/api_fastapi/security.py +32 -0
  10. airflow/providers/fab/auth_manager/api_fastapi/services/login.py +12 -25
  11. airflow/providers/fab/auth_manager/api_fastapi/services/roles.py +158 -0
  12. airflow/providers/fab/auth_manager/api_fastapi/sorting.py +49 -0
  13. airflow/providers/fab/auth_manager/fab_auth_manager.py +33 -3
  14. airflow/providers/fab/auth_manager/models/__init__.py +7 -21
  15. airflow/providers/fab/auth_manager/models/db.py +1 -1
  16. airflow/providers/fab/auth_manager/security_manager/override.py +55 -13
  17. airflow/providers/fab/migrations/versions/{0001_1_4_0_create_ab_tables_if_missing.py → 0000_1_4_0_create_ab_tables_if_missing.py} +4 -4
  18. airflow/providers/fab/version_compat.py +1 -0
  19. airflow/providers/fab/www/api_connexion/parameters.py +1 -46
  20. airflow/providers/fab/www/app.py +13 -10
  21. airflow/providers/fab/www/extensions/init_appbuilder.py +5 -2
  22. airflow/providers/fab/www/extensions/init_security.py +1 -1
  23. airflow/providers/fab/www/package-lock.json +390 -266
  24. airflow/providers/fab/www/package.json +9 -9
  25. airflow/providers/fab/www/session.py +5 -8
  26. airflow/providers/fab/www/static/dist/{743.fc7a7c6ef9d09365976e.js → 743.0c0bf201ae17e66a9a3f.js} +1 -1
  27. airflow/providers/fab/www/static/dist/{main.3cf3be1a0c5439bb640d.js → main.bc1f701c3d133e2a3bab.js} +1 -1
  28. airflow/providers/fab/www/static/dist/manifest.json +13 -13
  29. airflow/providers/fab/www/static/dist/runtime.254c277d91ce3ac79c64.js +1 -0
  30. airflow/providers/fab/www/views.py +19 -9
  31. {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/METADATA +14 -13
  32. {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/RECORD +49 -43
  33. airflow/providers/fab/www/static/dist/runtime.ad800fc1845ad5c6ddeb.js +0 -1
  34. /airflow/providers/fab/www/static/dist/{743.fc7a7c6ef9d09365976e.js.LICENSE.txt → 743.0c0bf201ae17e66a9a3f.js.LICENSE.txt} +0 -0
  35. /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ff5a35f322070b094aa2.css → airflowDefaultTheme.ef6fc04c9b6920cd75c9.css} +0 -0
  36. /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ff5a35f322070b094aa2.js → airflowDefaultTheme.ef6fc04c9b6920cd75c9.js} +0 -0
  37. /airflow/providers/fab/www/static/dist/{flash.5583a9e0cf11f2be93da.css → flash.eaaf777ec1b3628cf7be.css} +0 -0
  38. /airflow/providers/fab/www/static/dist/{flash.5583a9e0cf11f2be93da.js → flash.eaaf777ec1b3628cf7be.js} +0 -0
  39. /airflow/providers/fab/www/static/dist/{loadingDots.2e5f555f0753107b0300.css → loadingDots.76f4332c0a932c3dc08f.css} +0 -0
  40. /airflow/providers/fab/www/static/dist/{loadingDots.2e5f555f0753107b0300.js → loadingDots.76f4332c0a932c3dc08f.js} +0 -0
  41. /airflow/providers/fab/www/static/dist/{main.3cf3be1a0c5439bb640d.css → main.bc1f701c3d133e2a3bab.css} +0 -0
  42. /airflow/providers/fab/www/static/dist/{main.3cf3be1a0c5439bb640d.js.LICENSE.txt → main.bc1f701c3d133e2a3bab.js.LICENSE.txt} +0 -0
  43. /airflow/providers/fab/www/static/dist/{materialIcons.3e67dd6fbfcc4f3b5105.css → materialIcons.ad07a489b2f0fc1a96bf.css} +0 -0
  44. /airflow/providers/fab/www/static/dist/{materialIcons.3e67dd6fbfcc4f3b5105.js → materialIcons.ad07a489b2f0fc1a96bf.js} +0 -0
  45. /airflow/providers/fab/www/static/dist/{moment.9baee5ec3d7639a10897.js → moment.5b85b4f6be2fe9c405ac.js} +0 -0
  46. {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/WHEEL +0 -0
  47. {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/entry_points.txt +0 -0
  48. {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
  49. {airflow/providers/fab → apache_airflow_providers_fab-3.1.0rc1.dist-info/licenses}/LICENSE +0 -0
  50. {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/licenses/NOTICE +0 -0
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import copy
21
21
  import datetime
22
+ import importlib
22
23
  import itertools
23
24
  import logging
24
25
  import uuid
@@ -59,13 +60,14 @@ from flask_jwt_extended import JWTManager
59
60
  from flask_login import LoginManager
60
61
  from itsdangerous import want_bytes
61
62
  from markupsafe import Markup, escape
63
+ from packaging.version import Version
62
64
  from sqlalchemy import delete, func, inspect, or_, select
63
65
  from sqlalchemy.exc import MultipleResultsFound
64
66
  from sqlalchemy.orm import joinedload
65
67
  from werkzeug.security import check_password_hash, generate_password_hash
66
68
 
67
69
  from airflow.configuration import conf
68
- from airflow.exceptions import AirflowException
70
+ from airflow.providers.common.compat.sdk import AirflowException
69
71
  from airflow.providers.fab.auth_manager.models import (
70
72
  Action,
71
73
  Group,
@@ -122,11 +124,17 @@ if AIRFLOW_V_3_1_PLUS:
122
124
  with create_session() as session:
123
125
  yield from DBDagBag().iter_all_latest_version_dags(session=session)
124
126
  else:
125
- from airflow.models.dagbag import DagBag # type: ignore[attr-defined, no-redef]
127
+ try:
128
+ from airflow.models.dagbag import DagBag
129
+ except (ImportError, AttributeError):
130
+ DagBag = None
126
131
 
127
132
  def _iter_dags() -> Iterable[DAG | SerializedDAG]:
128
- dagbag = DagBag(read_dags_from_db=True) # type: ignore[call-arg]
129
- dagbag.collect_dags_from_db() # type: ignore[attr-defined]
133
+ if DagBag is None:
134
+ return []
135
+ dagbag = DagBag(read_dags_from_db=True)
136
+ if hasattr(dagbag, "collect_dags_from_db"):
137
+ dagbag.collect_dags_from_db()
130
138
  return dagbag.dags.values()
131
139
 
132
140
 
@@ -725,6 +733,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
725
733
  """The JMESPATH role to use for user registration."""
726
734
  return current_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
727
735
 
736
+ @property
737
+ def auth_remote_user_env_var(self) -> str:
738
+ return current_app.config["AUTH_REMOTE_USER_ENV_VAR"]
739
+
728
740
  @property
729
741
  def auth_username_ci(self):
730
742
  """Get the auth username for CI."""
@@ -790,10 +802,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
790
802
  current_app.config.setdefault("AUTH_ROLES_SYNC_AT_LOGIN", False)
791
803
  current_app.config.setdefault("AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS", False)
792
804
 
793
- from packaging.version import Version
794
- from werkzeug import __version__ as werkzeug_version
795
-
796
- parsed_werkzeug_version = Version(werkzeug_version)
805
+ parsed_werkzeug_version = Version(importlib.metadata.version("werkzeug"))
797
806
  if parsed_werkzeug_version < Version("3.0.0"):
798
807
  current_app.config.setdefault("FAB_PASSWORD_HASH_METHOD", "pbkdf2:sha256")
799
808
  current_app.config.setdefault(
@@ -1040,7 +1049,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1040
1049
  self.remove_permission_from_role(role, perm)
1041
1050
 
1042
1051
  # Adding the access control permissions
1043
- for rolename, resource_actions in access_control.items():
1052
+ for rolename, resource_actions_raw in access_control.items():
1044
1053
  role = self.find_role(rolename)
1045
1054
  if not role:
1046
1055
  raise AirflowException(
@@ -1048,9 +1057,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1048
1057
  f"'{rolename}', but that role does not exist"
1049
1058
  )
1050
1059
 
1051
- if not isinstance(resource_actions, dict):
1052
- # Support for old-style access_control where only the actions are specified
1053
- resource_actions = {permissions.RESOURCE_DAG: set(resource_actions)}
1060
+ # Support for old-style access_control where only the actions are specified
1061
+ resource_actions = (
1062
+ resource_actions_raw
1063
+ if isinstance(resource_actions_raw, dict)
1064
+ else {permissions.RESOURCE_DAG: set(resource_actions_raw)}
1065
+ )
1054
1066
 
1055
1067
  for resource_name, actions in resource_actions.items():
1056
1068
  if resource_name not in self.RESOURCE_DETAILS_MAP:
@@ -1644,7 +1656,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1644
1656
  return perm
1645
1657
  resource = self.create_resource(resource_name)
1646
1658
  if resource is None:
1647
- log.error(const.LOGMSG_ERR_SEC_ADD_PERMVIEW, f"Resource creation failed {resource_name}")
1659
+ log.error(const.LOGMSG_ERR_SEC_ADD_PERMVIEW, "Resource creation failed %s", resource_name)
1648
1660
  return None
1649
1661
  action = self.create_action(action_name)
1650
1662
  perm = self.permission_model()
@@ -2205,6 +2217,36 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2205
2217
  # decode - if empty string, default to fallback, otherwise take first element
2206
2218
  return raw_value[0].decode("utf-8") or fallback
2207
2219
 
2220
+ def auth_user_remote_user(self, username):
2221
+ """
2222
+ REMOTE_USER user Authentication.
2223
+
2224
+ :param username: user's username for remote auth
2225
+ """
2226
+ user = self.find_user(username=username)
2227
+
2228
+ # User does not exist, create one if auto user registration.
2229
+ if user is None and self.auth_user_registration:
2230
+ user = self.add_user(
2231
+ # All we have is REMOTE_USER, so we set
2232
+ # the other fields to blank.
2233
+ username=username,
2234
+ first_name=username,
2235
+ last_name="-",
2236
+ email=username + "@email.notfound",
2237
+ role=self.find_role(self.auth_user_registration_role),
2238
+ )
2239
+
2240
+ # If user does not exist on the DB and not auto user registration,
2241
+ # or user is inactive, go away.
2242
+ elif user is None or (not user.is_active):
2243
+ log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
2244
+ return None
2245
+
2246
+ self._rotate_session_id()
2247
+ self.update_user_auth_stat(user)
2248
+ return user
2249
+
2208
2250
  """
2209
2251
  ---------------
2210
2252
  Private methods
@@ -139,8 +139,8 @@ def upgrade() -> None:
139
139
  if_not_exists=True,
140
140
  )
141
141
  with op.batch_alter_table("ab_group_role", schema=None) as batch_op:
142
- batch_op.create_index("idx_group_id", ["group_id"], unique=False)
143
- batch_op.create_index("idx_group_role_id", ["role_id"], unique=False)
142
+ batch_op.create_index("idx_group_id", ["group_id"], unique=False, if_not_exists=True)
143
+ batch_op.create_index("idx_group_role_id", ["role_id"], unique=False, if_not_exists=True)
144
144
 
145
145
  op.create_table(
146
146
  "ab_permission_view",
@@ -175,8 +175,8 @@ def upgrade() -> None:
175
175
  if_not_exists=True,
176
176
  )
177
177
  with op.batch_alter_table("ab_user_group", schema=None) as batch_op:
178
- batch_op.create_index("idx_user_group_id", ["group_id"], unique=False)
179
- batch_op.create_index("idx_user_id", ["user_id"], unique=False)
178
+ batch_op.create_index("idx_user_group_id", ["group_id"], unique=False, if_not_exists=True)
179
+ batch_op.create_index("idx_user_id", ["user_id"], unique=False, if_not_exists=True)
180
180
 
181
181
  op.create_table(
182
182
  "ab_user_role",
@@ -33,3 +33,4 @@ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
33
33
 
34
34
 
35
35
  AIRFLOW_V_3_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 0)
36
+ AIRFLOW_V_3_1_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 1)
@@ -17,21 +17,16 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  import logging
20
- from collections.abc import Callable, Container
20
+ from collections.abc import Callable
21
21
  from functools import wraps
22
22
  from typing import TYPE_CHECKING, Any, TypeVar, cast
23
23
 
24
- from pendulum.parsing import ParserError
25
- from sqlalchemy import text
26
-
27
24
  from airflow.configuration import conf
28
25
  from airflow.providers.fab.www.api_connexion.exceptions import BadRequest
29
- from airflow.utils import timezone
30
26
 
31
27
  if TYPE_CHECKING:
32
28
  from datetime import datetime
33
29
 
34
- from sqlalchemy.sql import Select
35
30
 
36
31
  log = logging.getLogger(__name__)
37
32
 
@@ -42,24 +37,6 @@ def validate_istimezone(value: datetime) -> None:
42
37
  raise BadRequest("Invalid datetime format", detail="Naive datetime is disallowed")
43
38
 
44
39
 
45
- def format_datetime(value: str) -> datetime:
46
- """
47
- Format datetime objects.
48
-
49
- Datetime format parser for args since connexion doesn't parse datetimes
50
- https://github.com/zalando/connexion/issues/476
51
-
52
- This should only be used within connection views because it raises 400
53
- """
54
- value = value.strip()
55
- if value[-1] != "Z":
56
- value = value.replace(" ", "+")
57
- try:
58
- return timezone.parse(value)
59
- except (ParserError, TypeError) as err:
60
- raise BadRequest("Incorrect datetime argument", detail=str(err))
61
-
62
-
63
40
  def check_limit(value: int) -> int:
64
41
  """
65
42
  Check the limit does not exceed configured value.
@@ -107,25 +84,3 @@ def format_parameters(params_formatters: dict[str, Callable[[Any], Any]]) -> Cal
107
84
  return cast("T", wrapped_function)
108
85
 
109
86
  return format_parameters_decorator
110
-
111
-
112
- def apply_sorting(
113
- query: Select,
114
- order_by: str,
115
- to_replace: dict[str, str] | None = None,
116
- allowed_attrs: Container[str] | None = None,
117
- ) -> Select:
118
- """Apply sorting to query."""
119
- lstriped_orderby = order_by.lstrip("-")
120
- if allowed_attrs and lstriped_orderby not in allowed_attrs:
121
- raise BadRequest(
122
- detail=f"Ordering with '{lstriped_orderby}' is disallowed or "
123
- f"the attribute does not exist on the model"
124
- )
125
- if to_replace:
126
- lstriped_orderby = to_replace.get(lstriped_orderby, lstriped_orderby)
127
- if order_by[0] == "-":
128
- order_by = f"{lstriped_orderby} desc"
129
- else:
130
- order_by = f"{lstriped_orderby} asc"
131
- return query.order_by(text(order_by))
@@ -18,6 +18,7 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  from datetime import timedelta
21
+ from functools import cache
21
22
  from os.path import isabs
22
23
 
23
24
  from flask import Flask
@@ -30,7 +31,7 @@ from airflow.api_fastapi.app import get_auth_manager
30
31
  from airflow.configuration import conf
31
32
  from airflow.exceptions import AirflowConfigException
32
33
  from airflow.logging_config import configure_logging
33
- from airflow.providers.fab.www.extensions.init_appbuilder import init_appbuilder
34
+ from airflow.providers.fab.www.extensions.init_appbuilder import AirflowAppBuilder
34
35
  from airflow.providers.fab.www.extensions.init_jinja_globals import init_jinja_globals
35
36
  from airflow.providers.fab.www.extensions.init_manifest_files import configure_manifest_files
36
37
  from airflow.providers.fab.www.extensions.init_security import init_api_auth
@@ -44,8 +45,6 @@ from airflow.providers.fab.www.extensions.init_views import (
44
45
  from airflow.providers.fab.www.extensions.init_wsgi_middlewares import init_wsgi_middleware
45
46
  from airflow.providers.fab.www.utils import get_session_lifetime_config
46
47
 
47
- app: Flask | None = None
48
-
49
48
  # Initializes at the module level, so plugins can access it.
50
49
  # See: /docs/plugins.rst
51
50
  csrf = CSRFProtect()
@@ -85,6 +84,8 @@ def create_app(enable_plugins: bool):
85
84
  csrf.init_app(flask_app)
86
85
 
87
86
  db = SQLAlchemy(flask_app)
87
+ if settings.Session is None:
88
+ raise RuntimeError("Session not configured. Call configure_orm() first.")
88
89
  db.session = settings.Session
89
90
 
90
91
  configure_logging()
@@ -92,7 +93,12 @@ def create_app(enable_plugins: bool):
92
93
  init_api_auth(flask_app)
93
94
 
94
95
  with flask_app.app_context():
95
- init_appbuilder(flask_app, enable_plugins=enable_plugins)
96
+ AirflowAppBuilder(
97
+ app=flask_app,
98
+ session=db.session(),
99
+ base_template="airflow/main.html",
100
+ enable_plugins=enable_plugins,
101
+ )
96
102
  init_error_handlers(flask_app)
97
103
  # In two scenarios a Flask application can be created:
98
104
  # - To support Airflow 2 plugins relying on Flask (``enable_plugins`` is True)
@@ -112,15 +118,12 @@ def create_app(enable_plugins: bool):
112
118
  return flask_app
113
119
 
114
120
 
121
+ @cache
115
122
  def cached_app():
116
123
  """Return cached instance of Airflow WWW app."""
117
- global app
118
- if not app:
119
- app = create_app()
120
- return app
124
+ return create_app()
121
125
 
122
126
 
123
127
  def purge_cached_app():
124
128
  """Remove the cached version of the app in global state."""
125
- global app
126
- app = None
129
+ cached_app.cache_clear()
@@ -42,7 +42,7 @@ from airflow import settings
42
42
  from airflow.api_fastapi.app import create_auth_manager, get_auth_manager
43
43
  from airflow.configuration import conf
44
44
  from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
45
- from airflow.providers.fab.www.views import FabIndexView
45
+ from airflow.providers.fab.www.views import FabIndexView, redirect
46
46
 
47
47
  if TYPE_CHECKING:
48
48
  from flask import Flask
@@ -216,6 +216,7 @@ class AirflowAppBuilder:
216
216
  from airflow.providers.fab.www.views import get_safe_url
217
217
 
218
218
  fab_sec_views.get_safe_redirect = get_safe_url
219
+ fab_sec_views.redirect = redirect
219
220
 
220
221
  def _init_extension(self, app):
221
222
  app.appbuilder = self
@@ -473,7 +474,7 @@ class AirflowAppBuilder:
473
474
  baseview=baseview,
474
475
  cond=cond,
475
476
  )
476
- if self.app:
477
+ if current_app:
477
478
  self._add_permissions_menu(name)
478
479
  if category:
479
480
  self._add_permissions_menu(category)
@@ -592,6 +593,8 @@ class AirflowAppBuilder:
592
593
 
593
594
  def init_appbuilder(app: Flask, enable_plugins: bool) -> AirflowAppBuilder:
594
595
  """Init `Flask App Builder <https://flask-appbuilder.readthedocs.io/en/latest/>`__."""
596
+ if settings.Session is None:
597
+ raise RuntimeError("Session not configured. Call configure_orm() first.")
595
598
  return AirflowAppBuilder(
596
599
  app=app,
597
600
  session=settings.Session(),
@@ -20,7 +20,7 @@ import logging
20
20
  from importlib import import_module
21
21
 
22
22
  from airflow.configuration import conf
23
- from airflow.exceptions import AirflowException
23
+ from airflow.providers.common.compat.sdk import AirflowException
24
24
 
25
25
  log = logging.getLogger(__name__)
26
26