apache-airflow-providers-fab 2.2.1rc1__py3-none-any.whl → 2.3.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 (46) hide show
  1. airflow/providers/fab/__init__.py +1 -1
  2. airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +2 -1
  3. airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +2 -1
  4. airflow/providers/fab/auth_manager/api/auth/backend/session.py +2 -1
  5. airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml +2 -2
  6. airflow/providers/fab/auth_manager/api_fastapi/services/login.py +17 -8
  7. airflow/providers/fab/auth_manager/fab_auth_manager.py +2 -2
  8. airflow/providers/fab/auth_manager/security_manager/override.py +15 -7
  9. airflow/providers/fab/version_compat.py +35 -0
  10. airflow/providers/fab/www/api_connexion/parameters.py +2 -2
  11. airflow/providers/fab/www/api_connexion/security.py +2 -1
  12. airflow/providers/fab/www/api_connexion/types.py +5 -7
  13. airflow/providers/fab/www/auth.py +2 -2
  14. airflow/providers/fab/www/extensions/init_views.py +9 -5
  15. airflow/providers/fab/www/package-lock.json +174 -130
  16. airflow/providers/fab/www/package.json +4 -4
  17. airflow/providers/fab/www/security_manager.py +1 -1
  18. airflow/providers/fab/www/static/dist/{743.b6629eaae7f541f4158f.js → 743.27a753a06671118f1c5c.js} +2 -2
  19. airflow/providers/fab/www/static/dist/{airflowDefaultTheme.5a4eda684d2f5f33fa6a.js → airflowDefaultTheme.56d4475fdae7883d3454.js} +1 -1
  20. airflow/providers/fab/www/static/dist/{flash.9f756672299e9e57a824.js → flash.0951d47c62bc8906be65.js} +1 -1
  21. airflow/providers/fab/www/static/dist/jquery-ui.min.js +1 -1
  22. airflow/providers/fab/www/static/dist/{loadingDots.fc35e1feb443bd3a8d80.js → loadingDots.deaad0ce0e7691ed6251.js} +1 -1
  23. airflow/providers/fab/www/static/dist/main.810554d06c3e30f2484e.js +2 -0
  24. airflow/providers/fab/www/static/dist/manifest.json +13 -13
  25. airflow/providers/fab/www/static/dist/materialIcons.b0c6cc32cdacff89f7c2.js +1 -0
  26. airflow/providers/fab/www/static/dist/moment.518a43bcfaf149ae2836.js +1 -0
  27. airflow/providers/fab/www/static/dist/runtime.4a925577de9ab84d8e00.js +1 -0
  28. airflow/providers/fab/www/static/pin_100.png +0 -0
  29. airflow/providers/fab/www/static/pin_32.png +0 -0
  30. {apache_airflow_providers_fab-2.2.1rc1.dist-info → apache_airflow_providers_fab-2.3.0rc1.dist-info}/METADATA +8 -9
  31. {apache_airflow_providers_fab-2.2.1rc1.dist-info → apache_airflow_providers_fab-2.3.0rc1.dist-info}/RECORD +42 -39
  32. airflow/providers/fab/www/static/dist/main.26e36c3998f451900260.js +0 -2
  33. airflow/providers/fab/www/static/dist/materialIcons.e84a12f5be6e8d456d02.js +0 -1
  34. airflow/providers/fab/www/static/dist/moment.9023635a87b836172067.js +0 -1
  35. airflow/providers/fab/www/static/dist/runtime.78bb8f6146c25d99b9e2.js +0 -1
  36. /airflow/providers/fab/www/static/dist/{743.b6629eaae7f541f4158f.js.LICENSE.txt → 743.27a753a06671118f1c5c.js.LICENSE.txt} +0 -0
  37. /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.5a4eda684d2f5f33fa6a.css → airflowDefaultTheme.56d4475fdae7883d3454.css} +0 -0
  38. /airflow/providers/fab/www/static/dist/{flash.9f756672299e9e57a824.css → flash.0951d47c62bc8906be65.css} +0 -0
  39. /airflow/providers/fab/www/static/dist/{loadingDots.fc35e1feb443bd3a8d80.css → loadingDots.deaad0ce0e7691ed6251.css} +0 -0
  40. /airflow/providers/fab/www/static/dist/{main.26e36c3998f451900260.css → main.810554d06c3e30f2484e.css} +0 -0
  41. /airflow/providers/fab/www/static/dist/{main.26e36c3998f451900260.js.LICENSE.txt → main.810554d06c3e30f2484e.js.LICENSE.txt} +0 -0
  42. /airflow/providers/fab/www/static/dist/{materialIcons.e84a12f5be6e8d456d02.css → materialIcons.b0c6cc32cdacff89f7c2.css} +0 -0
  43. {apache_airflow_providers_fab-2.2.1rc1.dist-info → apache_airflow_providers_fab-2.3.0rc1.dist-info}/WHEEL +0 -0
  44. {apache_airflow_providers_fab-2.2.1rc1.dist-info → apache_airflow_providers_fab-2.3.0rc1.dist-info}/entry_points.txt +0 -0
  45. {apache_airflow_providers_fab-2.2.1rc1.dist-info → apache_airflow_providers_fab-2.3.0rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
  46. {apache_airflow_providers_fab-2.2.1rc1.dist-info → apache_airflow_providers_fab-2.3.0rc1.dist-info}/licenses/NOTICE +0 -0
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "2.2.1"
32
+ __version__ = "2.3.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "3.0.2"
@@ -18,8 +18,9 @@
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ from collections.abc import Callable
21
22
  from functools import wraps
22
- from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
23
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
23
24
 
24
25
  from flask import Response, current_app, request
25
26
  from flask_appbuilder.const import AUTH_LDAP
@@ -19,8 +19,9 @@ from __future__ import annotations
19
19
 
20
20
  import logging
21
21
  import os
22
+ from collections.abc import Callable
22
23
  from functools import wraps
23
- from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, cast
24
+ from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar, cast
24
25
 
25
26
  import kerberos
26
27
  from flask import Response, current_app, g, make_response, request
@@ -18,8 +18,9 @@
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ from collections.abc import Callable
21
22
  from functools import wraps
22
- from typing import Any, Callable, TypeVar, cast
23
+ from typing import Any, TypeVar, cast
23
24
 
24
25
  from flask import Response
25
26
 
@@ -6,7 +6,7 @@ info:
6
6
  endpoints to manage users and permissions managed by the FAB auth manager.
7
7
  version: 0.1.0
8
8
  paths:
9
- /token:
9
+ /auth/token:
10
10
  post:
11
11
  tags:
12
12
  - FabAuthManager
@@ -44,7 +44,7 @@ paths:
44
44
  application/json:
45
45
  schema:
46
46
  $ref: '#/components/schemas/HTTPValidationError'
47
- /token/cli:
47
+ /auth/token/cli:
48
48
  post:
49
49
  tags:
50
50
  - FabAuthManager
@@ -18,6 +18,7 @@ from __future__ import annotations
18
18
 
19
19
  from typing import TYPE_CHECKING, cast
20
20
 
21
+ from flask_appbuilder.const import AUTH_LDAP
21
22
  from starlette import status
22
23
  from starlette.exceptions import HTTPException
23
24
 
@@ -44,14 +45,22 @@ class FABAuthManagerLogin:
44
45
  )
45
46
 
46
47
  auth_manager = cast("FabAuthManager", get_auth_manager())
47
- user: User = auth_manager.security_manager.find_user(username=body.username)
48
+ user: User | None = None
49
+
50
+ if auth_manager.security_manager.auth_type == AUTH_LDAP:
51
+ user = auth_manager.security_manager.auth_user_ldap(
52
+ body.username, body.password, rotate_session_id=False
53
+ )
54
+ if user is None:
55
+ user = auth_manager.security_manager.auth_user_db(
56
+ body.username, body.password, rotate_session_id=False
57
+ )
58
+
48
59
  if not user:
49
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username")
60
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
50
61
 
51
- if auth_manager.security_manager.check_password(username=body.username, password=body.password):
52
- return LoginResponse(
53
- access_token=auth_manager.generate_jwt(
54
- user=user, expiration_time_in_seconds=expiration_time_in_seconds
55
- )
62
+ return LoginResponse(
63
+ access_token=auth_manager.generate_jwt(
64
+ user=user, expiration_time_in_seconds=expiration_time_in_seconds
56
65
  )
57
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password")
66
+ )
@@ -308,9 +308,9 @@ class FabAuthManager(BaseAuthManager[User]):
308
308
 
309
309
  There are multiple scenarios:
310
310
 
311
- 1. ``dag_access`` is not provided which means the user wants to access the DAG itself and not a sub
311
+ 1. ``access_entity`` is not provided which means the user wants to access the DAG itself and not a sub
312
312
  entity (e.g. DAG runs).
313
- 2. ``dag_access`` is provided which means the user wants to access a sub entity of the DAG
313
+ 2. ``access_entity`` is provided which means the user wants to access a sub entity of the DAG
314
314
  (e.g. DAG runs).
315
315
 
316
316
  a. If ``method`` is GET, then check the user has READ permissions on the DAG and the sub entity.
@@ -62,7 +62,7 @@ from flask_babel import lazy_gettext
62
62
  from flask_jwt_extended import JWTManager
63
63
  from flask_login import LoginManager
64
64
  from itsdangerous import want_bytes
65
- from markupsafe import Markup
65
+ from markupsafe import Markup, escape
66
66
  from sqlalchemy import func, inspect, or_, select
67
67
  from sqlalchemy.exc import MultipleResultsFound
68
68
  from sqlalchemy.orm import joinedload
@@ -547,8 +547,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
547
547
  user_session_model = interface.sql_session_model
548
548
  num_sessions = session.query(user_session_model).count()
549
549
  if num_sessions > MAX_NUM_DATABASE_USER_SESSIONS:
550
+ safe_username = escape(user.username)
550
551
  self._cli_safe_flash(
551
- f"The old sessions for user {user.username} have <b>NOT</b> been deleted!<br>"
552
+ f"The old sessions for user {safe_username} have <b>NOT</b> been deleted!<br>"
552
553
  f"You have a lot ({num_sessions}) of user sessions in the 'SESSIONS' table in "
553
554
  f"your database.<br> "
554
555
  "This indicates that this deployment might have an automated API calls that create "
@@ -565,9 +566,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
565
566
  session.delete(s)
566
567
  session.commit()
567
568
  else:
569
+ safe_username = escape(user.username)
568
570
  self._cli_safe_flash(
569
571
  "Since you are using `securecookie` session backend mechanism, we cannot prevent "
570
- f"some old sessions for user {user.username} to be reused.<br> If you want to make sure "
572
+ f"some old sessions for user {safe_username} to be reused.<br> If you want to make sure "
571
573
  "that the user is logged out from all sessions, you should consider using "
572
574
  "`database` session backend mechanism.<br> You can also change the 'secret_key` "
573
575
  "webserver configuration for all your webserver instances and restart the webserver. "
@@ -784,12 +786,14 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
784
786
 
785
787
  parsed_werkzeug_version = Version(werkzeug_version)
786
788
  if parsed_werkzeug_version < Version("3.0.0"):
789
+ app.config.setdefault("FAB_PASSWORD_HASH_METHOD", "pbkdf2:sha256")
787
790
  app.config.setdefault(
788
791
  "AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
789
792
  "pbkdf2:sha256:150000$Z3t6fmj2$22da622d94a1f8118"
790
793
  "c0976a03d2f18f680bfff877c9a965db9eedc51bc0be87c",
791
794
  )
792
795
  else:
796
+ app.config.setdefault("FAB_PASSWORD_HASH_METHOD", "scrypt")
793
797
  app.config.setdefault(
794
798
  "AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
795
799
  "scrypt:32768:8:1$wiDa0ruWlIPhp9LM$6e409d093e62ad54df2af895d0e125b05ff6cf6414"
@@ -1721,7 +1725,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1721
1725
  --------------------
1722
1726
  """
1723
1727
 
1724
- def auth_user_ldap(self, username, password):
1728
+ def auth_user_ldap(self, username, password, rotate_session_id=True):
1725
1729
  """
1726
1730
  Authenticate user with LDAP.
1727
1731
 
@@ -1888,7 +1892,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1888
1892
 
1889
1893
  # LOGIN SUCCESS (only if user is now registered)
1890
1894
  if user:
1891
- self._rotate_session_id()
1895
+ if rotate_session_id:
1896
+ self._rotate_session_id()
1892
1897
  self.update_user_auth_stat(user)
1893
1898
  return user
1894
1899
  return None
@@ -1917,7 +1922,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1917
1922
  return False
1918
1923
  return check_password_hash(user.password, password)
1919
1924
 
1920
- def auth_user_db(self, username, password):
1925
+ def auth_user_db(self, username, password, rotate_session_id=True):
1921
1926
  """
1922
1927
  Authenticate user, auth db style.
1923
1928
 
@@ -1925,6 +1930,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1925
1930
  The username or registered email address
1926
1931
  :param password:
1927
1932
  The password, will be tested against hashed password on db
1933
+ :param rotate_session_id:
1934
+ Whether to rotate the session ID
1928
1935
  """
1929
1936
  if username is None or username == "":
1930
1937
  return None
@@ -1940,7 +1947,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1940
1947
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
1941
1948
  return None
1942
1949
  if check_password_hash(user.password, password):
1943
- self._rotate_session_id()
1950
+ if rotate_session_id:
1951
+ self._rotate_session_id()
1944
1952
  self.update_user_auth_stat(user, True)
1945
1953
  return user
1946
1954
  self.update_user_auth_stat(user, False)
@@ -0,0 +1,35 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. 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,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+ #
18
+ # NOTE! THIS FILE IS COPIED MANUALLY IN OTHER PROVIDERS DELIBERATELY TO AVOID ADDING UNNECESSARY
19
+ # DEPENDENCIES BETWEEN PROVIDERS. IF YOU WANT TO ADD CONDITIONAL CODE IN YOUR PROVIDER THAT DEPENDS
20
+ # ON AIRFLOW VERSION, PLEASE COPY THIS FILE TO THE ROOT PACKAGE OF YOUR PROVIDER AND IMPORT
21
+ # THOSE CONSTANTS FROM IT RATHER THAN IMPORTING THEM FROM ANOTHER PROVIDER OR TEST CODE
22
+ #
23
+ from __future__ import annotations
24
+
25
+
26
+ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
27
+ from packaging.version import Version
28
+
29
+ from airflow import __version__
30
+
31
+ airflow_version = Version(__version__)
32
+ return airflow_version.major, airflow_version.minor, airflow_version.micro
33
+
34
+
35
+ AIRFLOW_V_3_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 0)
@@ -17,9 +17,9 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  import logging
20
- from collections.abc import Container
20
+ from collections.abc import Callable, Container
21
21
  from functools import wraps
22
- from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
22
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
23
23
 
24
24
  from pendulum.parsing import ParserError
25
25
  from sqlalchemy import text
@@ -16,8 +16,9 @@
16
16
  # under the License.
17
17
  from __future__ import annotations
18
18
 
19
+ from collections.abc import Callable
19
20
  from functools import wraps
20
- from typing import TYPE_CHECKING, Callable, TypeVar, cast
21
+ from typing import TYPE_CHECKING, TypeVar, cast
21
22
 
22
23
  from flask import Response, current_app
23
24
 
@@ -17,14 +17,12 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  from collections.abc import Mapping, Sequence
20
- from typing import Any, Optional, Union
20
+ from typing import Any
21
21
 
22
22
  from flask import Response
23
23
 
24
- APIResponse = Union[
25
- Response,
26
- tuple[object, int], # For '(NoContent, 201)'.
27
- Mapping[str, Any], # JSON.
28
- ]
24
+ # tuple[object, int] For '(NoContent, 201)'.
25
+ # Mapping[str, Any] for json
26
+ APIResponse = Response | tuple[object, int] | Mapping[str, Any]
29
27
 
30
- UpdateMask = Optional[Sequence[str]]
28
+ UpdateMask = Sequence[str] | None
@@ -18,9 +18,9 @@ from __future__ import annotations
18
18
 
19
19
  import functools
20
20
  import logging
21
- from collections.abc import Sequence
21
+ from collections.abc import Callable, Sequence
22
22
  from functools import wraps
23
- from typing import TYPE_CHECKING, Callable, TypeVar, cast
23
+ from typing import TYPE_CHECKING, TypeVar, cast
24
24
 
25
25
  from flask import flash, redirect, render_template, request, url_for
26
26
  from flask_appbuilder._compat import as_unicode
@@ -26,6 +26,7 @@ from connexion.exceptions import BadRequestProblem, ProblemException
26
26
  from flask import request
27
27
 
28
28
  from airflow.api_fastapi.app import get_auth_manager
29
+ from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_PLUS
29
30
  from airflow.providers.fab.www.api_connexion.exceptions import common_error_handler
30
31
 
31
32
  if TYPE_CHECKING:
@@ -120,11 +121,14 @@ def init_plugins(app):
120
121
  log.debug("Adding view %s without menu", str(type(view["view"])))
121
122
  appbuilder.add_view_no_menu(view["view"])
122
123
 
123
- for menu_link in sorted(
124
- plugins_manager.flask_appbuilder_menu_links, key=lambda x: (x.get("category", ""), x["name"])
125
- ):
126
- log.debug("Adding menu link %s to %s", menu_link["name"], menu_link["href"])
127
- appbuilder.add_link(**menu_link)
124
+ # Since Airflow 3.1 flask_appbuilder_menu_links are added to the Airflow 3 UI
125
+ # navbar..
126
+ if not AIRFLOW_V_3_1_PLUS:
127
+ for menu_link in sorted(
128
+ plugins_manager.flask_appbuilder_menu_links, key=lambda x: (x.get("category", ""), x["name"])
129
+ ):
130
+ log.debug("Adding menu link %s to %s", menu_link["name"], menu_link["href"])
131
+ appbuilder.add_link(**menu_link)
128
132
 
129
133
  for blue_print in plugins_manager.flask_blueprints:
130
134
  log.debug("Adding blueprint %s:%s", blue_print["name"], blue_print["blueprint"].import_name)