apache-airflow-providers-fab 3.0.1__py3-none-any.whl → 3.1.1rc1__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 (51) 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/cli_commands/permissions_command.py +6 -2
  14. airflow/providers/fab/auth_manager/fab_auth_manager.py +33 -3
  15. airflow/providers/fab/auth_manager/models/__init__.py +3 -8
  16. airflow/providers/fab/auth_manager/models/db.py +1 -1
  17. airflow/providers/fab/auth_manager/security_manager/override.py +60 -17
  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/extensions/init_views.py +11 -7
  24. airflow/providers/fab/www/package-lock.json +417 -265
  25. airflow/providers/fab/www/package.json +13 -10
  26. airflow/providers/fab/www/session.py +5 -8
  27. airflow/providers/fab/www/static/dist/{743.935ed3d26e56ed8f63d3.js → 743.0c0bf201ae17e66a9a3f.js} +1 -1
  28. airflow/providers/fab/www/static/dist/{main.3cf3be1a0c5439bb640d.js → main.bc1f701c3d133e2a3bab.js} +1 -1
  29. airflow/providers/fab/www/static/dist/manifest.json +13 -13
  30. airflow/providers/fab/www/views.py +18 -14
  31. {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/METADATA +15 -14
  32. {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/RECORD +51 -45
  33. /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} +0 -0
  34. /airflow/providers/fab/www/static/dist/{743.935ed3d26e56ed8f63d3.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. /airflow/providers/fab/www/static/dist/{runtime.6ad9da077ea169d60db9.js → runtime.254c277d91ce3ac79c64.js} +0 -0
  47. {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/WHEEL +0 -0
  48. {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/entry_points.txt +0 -0
  49. {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
  50. {airflow/providers/fab → apache_airflow_providers_fab-3.1.1rc1.dist-info/licenses}/LICENSE +0 -0
  51. {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,137 @@
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
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING
20
+
21
+ from fastapi import Depends, Path, Query, status
22
+
23
+ from airflow.api_fastapi.common.router import AirflowRouter
24
+ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
25
+ from airflow.providers.fab.auth_manager.api_fastapi.datamodels.roles import (
26
+ RoleBody,
27
+ RoleCollectionResponse,
28
+ RoleResponse,
29
+ )
30
+ from airflow.providers.fab.auth_manager.api_fastapi.parameters import get_effective_limit
31
+ from airflow.providers.fab.auth_manager.api_fastapi.security import requires_fab_custom_view
32
+ from airflow.providers.fab.auth_manager.api_fastapi.services.roles import FABAuthManagerRoles
33
+ from airflow.providers.fab.auth_manager.cli_commands.utils import get_application_builder
34
+ from airflow.providers.fab.www.security import permissions
35
+
36
+ if TYPE_CHECKING:
37
+ from airflow.providers.fab.auth_manager.api_fastapi.datamodels.roles import (
38
+ RoleBody,
39
+ RoleResponse,
40
+ )
41
+
42
+
43
+ roles_router = AirflowRouter(prefix="/fab/v1", tags=["FabAuthManager"])
44
+
45
+
46
+ @roles_router.post(
47
+ "/roles",
48
+ responses=create_openapi_http_exception_doc(
49
+ [
50
+ status.HTTP_400_BAD_REQUEST,
51
+ status.HTTP_401_UNAUTHORIZED,
52
+ status.HTTP_403_FORBIDDEN,
53
+ status.HTTP_409_CONFLICT,
54
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
55
+ ]
56
+ ),
57
+ dependencies=[Depends(requires_fab_custom_view("POST", permissions.RESOURCE_ROLE))],
58
+ )
59
+ def create_role(body: RoleBody) -> RoleResponse:
60
+ """Create a new role (actions can be empty)."""
61
+ with get_application_builder():
62
+ return FABAuthManagerRoles.create_role(body=body)
63
+
64
+
65
+ @roles_router.get(
66
+ "/roles",
67
+ response_model=RoleCollectionResponse,
68
+ responses=create_openapi_http_exception_doc(
69
+ [
70
+ status.HTTP_400_BAD_REQUEST,
71
+ status.HTTP_401_UNAUTHORIZED,
72
+ status.HTTP_403_FORBIDDEN,
73
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
74
+ ]
75
+ ),
76
+ dependencies=[Depends(requires_fab_custom_view("GET", permissions.RESOURCE_ROLE))],
77
+ )
78
+ def get_roles(
79
+ order_by: str = Query("name", description="Field to order by. Prefix with '-' for descending."),
80
+ limit: int = Depends(get_effective_limit()),
81
+ offset: int = Query(0, ge=0, description="Number of items to skip before starting to collect results."),
82
+ ) -> RoleCollectionResponse:
83
+ """List roles with pagination and ordering."""
84
+ with get_application_builder():
85
+ return FABAuthManagerRoles.get_roles(order_by=order_by, limit=limit, offset=offset)
86
+
87
+
88
+ @roles_router.delete(
89
+ "/roles/{name}",
90
+ responses=create_openapi_http_exception_doc(
91
+ [
92
+ status.HTTP_401_UNAUTHORIZED,
93
+ status.HTTP_403_FORBIDDEN,
94
+ status.HTTP_404_NOT_FOUND,
95
+ ]
96
+ ),
97
+ dependencies=[Depends(requires_fab_custom_view("DELETE", permissions.RESOURCE_ROLE))],
98
+ )
99
+ def delete_role(name: str = Path(..., min_length=1)) -> None:
100
+ """Delete an existing role."""
101
+ with get_application_builder():
102
+ return FABAuthManagerRoles.delete_role(name=name)
103
+
104
+
105
+ @roles_router.get(
106
+ "/roles/{name}",
107
+ responses=create_openapi_http_exception_doc(
108
+ [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN, status.HTTP_404_NOT_FOUND]
109
+ ),
110
+ dependencies=[Depends(requires_fab_custom_view("GET", permissions.RESOURCE_ROLE))],
111
+ )
112
+ def get_role(name: str = Path(..., min_length=1)) -> RoleResponse:
113
+ """Get an existing role."""
114
+ with get_application_builder():
115
+ return FABAuthManagerRoles.get_role(name=name)
116
+
117
+
118
+ @roles_router.patch(
119
+ "/roles/{name}",
120
+ responses=create_openapi_http_exception_doc(
121
+ [
122
+ status.HTTP_400_BAD_REQUEST,
123
+ status.HTTP_401_UNAUTHORIZED,
124
+ status.HTTP_403_FORBIDDEN,
125
+ status.HTTP_404_NOT_FOUND,
126
+ ]
127
+ ),
128
+ dependencies=[Depends(requires_fab_custom_view("PATCH", permissions.RESOURCE_ROLE))],
129
+ )
130
+ def patch_role(
131
+ body: RoleBody,
132
+ name: str = Path(..., min_length=1),
133
+ update_mask: str | None = Query(None, description="Comma-separated list of fields to update"),
134
+ ) -> RoleResponse:
135
+ """Update an existing role."""
136
+ with get_application_builder():
137
+ return FABAuthManagerRoles.patch_role(name=name, body=body, update_mask=update_mask)
@@ -0,0 +1,32 @@
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
+ from __future__ import annotations
18
+
19
+ from fastapi import Depends, HTTPException, status
20
+
21
+ from airflow.api_fastapi.app import get_auth_manager
22
+ from airflow.api_fastapi.core_api.security import get_user
23
+
24
+
25
+ def requires_fab_custom_view(method: str, resource_name: str):
26
+ def _check(user=Depends(get_user)):
27
+ if not get_auth_manager().is_authorized_custom_view(
28
+ method=method, resource_name=resource_name, user=user
29
+ ):
30
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
31
+
32
+ return _check
@@ -16,19 +16,14 @@
16
16
  # under the License.
17
17
  from __future__ import annotations
18
18
 
19
- from typing import TYPE_CHECKING, cast
19
+ from typing import Any
20
20
 
21
- from flask_appbuilder.const import AUTH_LDAP
22
21
  from starlette import status
23
22
  from starlette.exceptions import HTTPException
24
23
 
25
- from airflow.api_fastapi.app import get_auth_manager
26
24
  from airflow.configuration import conf
27
- from airflow.providers.fab.auth_manager.api_fastapi.datamodels.login import LoginBody, LoginResponse
28
-
29
- if TYPE_CHECKING:
30
- from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager
31
- from airflow.providers.fab.auth_manager.models import User
25
+ from airflow.providers.fab.auth_manager.api_fastapi.datamodels.login import LoginResponse
26
+ from airflow.providers.fab.www.utils import get_fab_auth_manager
32
27
 
33
28
 
34
29
  class FABAuthManagerLogin:
@@ -36,25 +31,17 @@ class FABAuthManagerLogin:
36
31
 
37
32
  @classmethod
38
33
  def create_token(
39
- cls, body: LoginBody, expiration_time_in_seconds: int = conf.getint("api_auth", "jwt_expiration_time")
34
+ cls,
35
+ headers: dict[str, str],
36
+ body: dict[str, Any],
37
+ expiration_time_in_seconds: int = conf.getint("api_auth", "jwt_expiration_time"),
40
38
  ) -> LoginResponse:
41
39
  """Create a new token."""
42
- if not body.username or not body.password:
43
- raise HTTPException(
44
- status_code=status.HTTP_400_BAD_REQUEST, detail="Username and password must be provided"
45
- )
46
-
47
- auth_manager = cast("FabAuthManager", get_auth_manager())
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
- )
40
+ auth_manager = get_fab_auth_manager()
41
+ try:
42
+ user = auth_manager.create_token(headers=headers, body=body)
43
+ except ValueError as e:
44
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
58
45
 
59
46
  if not user:
60
47
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
@@ -0,0 +1,158 @@
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
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING
20
+
21
+ from fastapi import HTTPException, status
22
+ from sqlalchemy import func, select
23
+
24
+ from airflow.providers.fab.auth_manager.api_fastapi.datamodels.roles import (
25
+ RoleBody,
26
+ RoleCollectionResponse,
27
+ RoleResponse,
28
+ )
29
+ from airflow.providers.fab.auth_manager.api_fastapi.sorting import build_ordering
30
+ from airflow.providers.fab.auth_manager.models import Role
31
+ from airflow.providers.fab.www.utils import get_fab_auth_manager
32
+
33
+ if TYPE_CHECKING:
34
+ from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
35
+
36
+
37
+ class FABAuthManagerRoles:
38
+ """Service layer for FAB Auth Manager role operations (create, validate, sync)."""
39
+
40
+ @staticmethod
41
+ def _check_action_and_resource(
42
+ security_manager: FabAirflowSecurityManagerOverride,
43
+ perms: list[tuple[str, str]],
44
+ ) -> None:
45
+ for action_name, resource_name in perms:
46
+ if not security_manager.get_action(action_name):
47
+ raise HTTPException(
48
+ status_code=status.HTTP_400_BAD_REQUEST,
49
+ detail=f"The specified action: {action_name!r} was not found",
50
+ )
51
+ if not security_manager.get_resource(resource_name):
52
+ raise HTTPException(
53
+ status_code=status.HTTP_400_BAD_REQUEST,
54
+ detail=f"The specified resource: {resource_name!r} was not found",
55
+ )
56
+
57
+ @classmethod
58
+ def create_role(cls, body: RoleBody) -> RoleResponse:
59
+ security_manager = get_fab_auth_manager().security_manager
60
+
61
+ existing = security_manager.find_role(name=body.name)
62
+ if existing:
63
+ raise HTTPException(
64
+ status_code=status.HTTP_409_CONFLICT,
65
+ detail=f"Role with name {body.name!r} already exists; please update with the PATCH endpoint",
66
+ )
67
+
68
+ perms: list[tuple[str, str]] = [(ar.action.name, ar.resource.name) for ar in (body.permissions or [])]
69
+
70
+ cls._check_action_and_resource(security_manager, perms)
71
+
72
+ security_manager.bulk_sync_roles([{"role": body.name, "perms": perms}])
73
+
74
+ created = security_manager.find_role(name=body.name)
75
+ if not created:
76
+ raise HTTPException(
77
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
78
+ detail="Role was not created due to an unexpected error.",
79
+ )
80
+
81
+ return RoleResponse.model_validate(created)
82
+
83
+ @classmethod
84
+ def get_roles(cls, *, order_by: str, limit: int, offset: int) -> RoleCollectionResponse:
85
+ security_manager = get_fab_auth_manager().security_manager
86
+ session = security_manager.session
87
+
88
+ total_entries = session.scalars(select(func.count(Role.id))).one()
89
+
90
+ ordering = build_ordering(order_by, allowed={"name": Role.name, "role_id": Role.id})
91
+
92
+ stmt = select(Role).order_by(ordering).offset(offset).limit(limit)
93
+ roles = session.scalars(stmt).unique().all()
94
+
95
+ return RoleCollectionResponse(
96
+ roles=[RoleResponse.model_validate(r) for r in roles],
97
+ total_entries=total_entries,
98
+ )
99
+
100
+ @classmethod
101
+ def delete_role(cls, name: str) -> None:
102
+ security_manager = get_fab_auth_manager().security_manager
103
+
104
+ existing = security_manager.find_role(name=name)
105
+ if not existing:
106
+ raise HTTPException(
107
+ status_code=status.HTTP_404_NOT_FOUND,
108
+ detail=f"Role with name {name!r} does not exist.",
109
+ )
110
+ security_manager.delete_role(existing)
111
+
112
+ @classmethod
113
+ def get_role(cls, name: str) -> RoleResponse:
114
+ security_manager = get_fab_auth_manager().security_manager
115
+
116
+ existing = security_manager.find_role(name=name)
117
+ if not existing:
118
+ raise HTTPException(
119
+ status_code=status.HTTP_404_NOT_FOUND,
120
+ detail=f"Role with name {name!r} does not exist.",
121
+ )
122
+ return RoleResponse.model_validate(existing)
123
+
124
+ @classmethod
125
+ def patch_role(cls, body: RoleBody, name: str, update_mask: str | None = None) -> RoleResponse:
126
+ security_manager = get_fab_auth_manager().security_manager
127
+
128
+ existing = security_manager.find_role(name=name)
129
+ if not existing:
130
+ raise HTTPException(
131
+ status_code=status.HTTP_404_NOT_FOUND,
132
+ detail=f"Role with name {name!r} does not exist.",
133
+ )
134
+
135
+ if update_mask:
136
+ update_data = RoleResponse.model_validate(existing)
137
+
138
+ for field in update_mask:
139
+ if field == "actions":
140
+ update_data.permissions = body.permissions
141
+ elif hasattr(body, field):
142
+ setattr(update_data, field, getattr(body, field))
143
+ else:
144
+ raise HTTPException(
145
+ status_code=status.HTTP_400_BAD_REQUEST,
146
+ detail=f"'{field}' in update_mask is unknown",
147
+ )
148
+ else:
149
+ update_data = RoleResponse(name=body.name, permissions=body.permissions or [])
150
+
151
+ perms: list[tuple[str, str]] = [(ar.action.name, ar.resource.name) for ar in (body.permissions or [])]
152
+ cls._check_action_and_resource(security_manager, perms)
153
+ security_manager.bulk_sync_roles([{"role": name, "perms": perms}])
154
+
155
+ new_name = update_data.name
156
+ if new_name and new_name != existing.name:
157
+ security_manager.update_role(role_id=existing.id, name=new_name)
158
+ return RoleResponse.model_validate(update_data)
@@ -0,0 +1,49 @@
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
+ from __future__ import annotations
18
+
19
+ from collections.abc import Mapping
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ from fastapi import HTTPException, status
23
+ from sqlalchemy import asc, desc
24
+
25
+ if TYPE_CHECKING:
26
+ from sqlalchemy.orm import InstrumentedAttribute
27
+ from sqlalchemy.sql.elements import ColumnElement
28
+
29
+
30
+ def build_ordering(
31
+ order_by: str, *, allowed: Mapping[str, ColumnElement[Any]] | Mapping[str, InstrumentedAttribute[Any]]
32
+ ) -> ColumnElement[Any]:
33
+ """
34
+ Build an SQLAlchemy ORDER BY expression from the `order_by` parameter.
35
+
36
+ :param order_by: Public field name, optionally prefixed with "-" for descending.
37
+ :param allowed: Map of public field to SQLAlchemy column/expression.
38
+ """
39
+ is_desc = order_by.startswith("-")
40
+ key = order_by.lstrip("-")
41
+
42
+ if key not in allowed:
43
+ raise HTTPException(
44
+ status_code=status.HTTP_400_BAD_REQUEST,
45
+ detail=f"Ordering with '{order_by}' is disallowed or the attribute does not exist on the model",
46
+ )
47
+
48
+ col = allowed[key]
49
+ return desc(col) if is_desc else asc(col)
@@ -50,7 +50,11 @@ def cleanup_dag_permissions(dag_id: str, session: Session = NEW_SESSION) -> None
50
50
  from sqlalchemy import delete, select
51
51
 
52
52
  from airflow.providers.fab.auth_manager.models import Permission, Resource, assoc_permission_role
53
- from airflow.security.permissions import RESOURCE_DAG_PREFIX, RESOURCE_DAG_RUN, RESOURCE_DETAILS_MAP
53
+ from airflow.providers.fab.www.security.permissions import (
54
+ RESOURCE_DAG_PREFIX,
55
+ RESOURCE_DAG_RUN,
56
+ RESOURCE_DETAILS_MAP,
57
+ )
54
58
 
55
59
  # Clean up specific DAG permissions
56
60
  dag_resources = session.scalars(
@@ -107,7 +111,7 @@ def permissions_cleanup(args):
107
111
  from airflow.models import DagModel
108
112
  from airflow.providers.fab.auth_manager.cli_commands.utils import get_application_builder
109
113
  from airflow.providers.fab.auth_manager.models import Resource
110
- from airflow.security.permissions import (
114
+ from airflow.providers.fab.www.security.permissions import (
111
115
  RESOURCE_DAG_PREFIX,
112
116
  RESOURCE_DAG_RUN,
113
117
  RESOURCE_DETAILS_MAP,
@@ -27,6 +27,7 @@ import packaging.version
27
27
  from connexion import FlaskApi
28
28
  from fastapi import FastAPI
29
29
  from flask import Blueprint, current_app, g
30
+ from flask_appbuilder.const import AUTH_LDAP
30
31
  from sqlalchemy import select
31
32
  from sqlalchemy.orm import Session, joinedload
32
33
  from starlette.middleware.wsgi import WSGIMiddleware
@@ -56,8 +57,9 @@ from airflow.cli.cli_config import (
56
57
  GroupCommand,
57
58
  )
58
59
  from airflow.configuration import conf
59
- from airflow.exceptions import AirflowConfigException, AirflowException
60
+ from airflow.exceptions import AirflowConfigException
60
61
  from airflow.models import Connection, DagModel, Pool, Variable
62
+ from airflow.providers.common.compat.sdk import AirflowException
61
63
  from airflow.providers.fab.auth_manager.cli_commands.definition import (
62
64
  DB_COMMANDS,
63
65
  PERMISSIONS_CLEANUP_COMMAND,
@@ -78,6 +80,7 @@ from airflow.providers.fab.www.security import permissions
78
80
  from airflow.providers.fab.www.security.permissions import (
79
81
  ACTION_CAN_READ,
80
82
  RESOURCE_AUDIT_LOG,
83
+ RESOURCE_BACKFILL,
81
84
  RESOURCE_CLUSTER_ACTIVITY,
82
85
  RESOURCE_CONFIG,
83
86
  RESOURCE_CONNECTION,
@@ -105,7 +108,6 @@ from airflow.providers.fab.www.utils import (
105
108
  get_fab_action_from_method_map,
106
109
  get_method_from_fab_action_map,
107
110
  )
108
- from airflow.security.permissions import RESOURCE_BACKFILL
109
111
  from airflow.utils.session import NEW_SESSION, create_session, provide_session
110
112
  from airflow.utils.yaml import safe_load
111
113
 
@@ -226,6 +228,7 @@ class FabAuthManager(BaseAuthManager[User]):
226
228
  from airflow.providers.fab.auth_manager.api_fastapi.routes.login import (
227
229
  login_router,
228
230
  )
231
+ from airflow.providers.fab.auth_manager.api_fastapi.routes.roles import roles_router
229
232
 
230
233
  flask_app = create_app(enable_plugins=False)
231
234
 
@@ -241,6 +244,7 @@ class FabAuthManager(BaseAuthManager[User]):
241
244
 
242
245
  # Add the login router to the FastAPI app
243
246
  app.include_router(login_router)
247
+ app.include_router(roles_router)
244
248
 
245
249
  app.mount("/", WSGIMiddleware(flask_app))
246
250
 
@@ -296,6 +300,32 @@ class FabAuthManager(BaseAuthManager[User]):
296
300
  or (not user.is_anonymous and user.is_active)
297
301
  )
298
302
 
303
+ def create_token(self, headers: dict[str, str], body: dict[str, Any]) -> User:
304
+ """
305
+ Create a new token from a payload.
306
+
307
+ By default, it uses basic authentication (username and password).
308
+ Override this method to use a different authentication method (e.g. oauth).
309
+
310
+ :param headers: request headers
311
+ :param body: request body
312
+ """
313
+ if not body.get("username") or not body.get("password"):
314
+ raise ValueError("Username and password must be provided")
315
+
316
+ user: User | None = None
317
+
318
+ if self.security_manager.auth_type == AUTH_LDAP:
319
+ user = self.security_manager.auth_user_ldap(
320
+ body["username"], body["password"], rotate_session_id=False
321
+ )
322
+ if user is None:
323
+ user = self.security_manager.auth_user_db(
324
+ body["username"], body["password"], rotate_session_id=False
325
+ )
326
+
327
+ return user
328
+
299
329
  def is_authorized_configuration(
300
330
  self,
301
331
  *,
@@ -567,7 +597,7 @@ class FabAuthManager(BaseAuthManager[User]):
567
597
 
568
598
  def get_url_logout(self) -> str | None:
569
599
  """Return the logout page url."""
570
- return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/logout/")
600
+ return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/logout")
571
601
 
572
602
  def register_views(self) -> None:
573
603
  self.security_manager.register_views()
@@ -21,8 +21,6 @@ import datetime
21
21
  # This product contains a modified portion of 'Flask App Builder' developed by Daniel Vaz Gaspar.
22
22
  # (https://github.com/dpgaspar/Flask-AppBuilder).
23
23
  # Copyright 2013, Daniel Vaz Gaspar
24
- from typing import TYPE_CHECKING
25
-
26
24
  from flask import current_app, g
27
25
  from flask_appbuilder import Model
28
26
  from sqlalchemy import (
@@ -45,12 +43,6 @@ from sqlalchemy.orm import Mapped, backref, declared_attr, relationship
45
43
  from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
46
44
  from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
47
45
 
48
- if TYPE_CHECKING:
49
- try:
50
- from sqlalchemy import Identity
51
- except Exception:
52
- Identity = None
53
-
54
46
  """
55
47
  Compatibility note: The models in this file are duplicated from Flask AppBuilder.
56
48
  """
@@ -157,6 +149,9 @@ class Resource(Model):
157
149
  def __eq__(self, other):
158
150
  return (isinstance(other, self.__class__)) and (self.name == other.name)
159
151
 
152
+ def __hash__(self):
153
+ return hash((self.id, self.name))
154
+
160
155
  def __neq__(self, other):
161
156
  return self.name != other.name
162
157
 
@@ -21,7 +21,7 @@ from pathlib import Path
21
21
  from flask_appbuilder import Model
22
22
 
23
23
  from airflow import settings
24
- from airflow.exceptions import AirflowException
24
+ from airflow.providers.common.compat.sdk import AirflowException
25
25
  from airflow.utils.db import _offline_migration, print_happy_cat
26
26
  from airflow.utils.db_manager import BaseDBManager
27
27