apache-airflow-providers-fab 2.4.4rc1__py3-none-any.whl → 3.0.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.
- airflow/providers/fab/__init__.py +1 -1
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +2 -2
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +4 -4
- airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +7 -4
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +1 -2
- airflow/providers/fab/auth_manager/cli_commands/utils.py +7 -3
- airflow/providers/fab/auth_manager/fab_auth_manager.py +15 -11
- airflow/providers/fab/auth_manager/models/__init__.py +179 -122
- airflow/providers/fab/auth_manager/models/db.py +11 -6
- airflow/providers/fab/auth_manager/security_manager/override.py +239 -213
- airflow/providers/fab/auth_manager/views/user.py +11 -5
- airflow/providers/fab/migrations/versions/0001_1_4_0_create_ab_tables_if_missing.py +1 -0
- airflow/providers/fab/www/app.py +3 -4
- airflow/providers/fab/www/extensions/init_appbuilder.py +26 -39
- airflow/providers/fab/www/extensions/init_session.py +2 -2
- airflow/providers/fab/www/security_appless.py +6 -1
- airflow/providers/fab/www/security_manager.py +4 -14
- airflow/providers/fab/www/session.py +26 -3
- airflow/providers/fab/www/utils.py +1 -208
- {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc1.dist-info}/METADATA +16 -10
- {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc1.dist-info}/RECORD +25 -25
- {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc1.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc1.dist-info}/entry_points.txt +0 -0
- {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.0rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
- {apache_airflow_providers_fab-2.4.4rc1.dist-info → apache_airflow_providers_fab-3.0.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__ = "
|
|
32
|
+
__version__ = "3.0.0"
|
|
33
33
|
|
|
34
34
|
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
|
|
35
35
|
"3.0.2"
|
|
@@ -72,7 +72,7 @@ def get_role(*, role_name: str) -> APIResponse:
|
|
|
72
72
|
def get_roles(*, order_by: str = "name", limit: int, offset: int | None = None) -> APIResponse:
|
|
73
73
|
"""Get roles."""
|
|
74
74
|
security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
|
|
75
|
-
session = security_manager.
|
|
75
|
+
session = security_manager.session
|
|
76
76
|
total_entries = session.scalars(select(func.count(Role.id))).one()
|
|
77
77
|
direction = desc if order_by.startswith("-") else asc
|
|
78
78
|
to_replace = {"role_id": "id"}
|
|
@@ -99,7 +99,7 @@ def get_roles(*, order_by: str = "name", limit: int, offset: int | None = None)
|
|
|
99
99
|
def get_permissions(*, limit: int, offset: int | None = None) -> APIResponse:
|
|
100
100
|
"""Get permissions."""
|
|
101
101
|
security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
|
|
102
|
-
session = security_manager.
|
|
102
|
+
session = security_manager.session
|
|
103
103
|
total_entries = session.scalars(select(func.count(Action.id))).one()
|
|
104
104
|
query = select(Action)
|
|
105
105
|
actions = session.scalars(query.offset(offset).limit(limit)).all()
|
|
@@ -56,10 +56,10 @@ def get_user(*, username: str) -> APIResponse:
|
|
|
56
56
|
|
|
57
57
|
@requires_access_custom_view("GET", permissions.RESOURCE_USER)
|
|
58
58
|
@format_parameters({"limit": check_limit})
|
|
59
|
-
def get_users(*, limit: int, order_by: str = "id", offset:
|
|
59
|
+
def get_users(*, limit: int, order_by: str = "id", offset: int | None = None) -> APIResponse:
|
|
60
60
|
"""Get users."""
|
|
61
61
|
security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
|
|
62
|
-
session = security_manager.
|
|
62
|
+
session = security_manager.session
|
|
63
63
|
total_entries = session.execute(select(func.count(User.id))).scalar()
|
|
64
64
|
direction = desc if order_by.startswith("-") else asc
|
|
65
65
|
to_replace = {"user_id": "id"}
|
|
@@ -212,7 +212,7 @@ def delete_user(*, username: str) -> APIResponse:
|
|
|
212
212
|
raise NotFound(title="User not found", detail=detail)
|
|
213
213
|
|
|
214
214
|
user.roles = [] # Clear foreign keys on this user first.
|
|
215
|
-
security_manager.
|
|
216
|
-
security_manager.
|
|
215
|
+
security_manager.session.delete(user)
|
|
216
|
+
security_manager.session.commit()
|
|
217
217
|
|
|
218
218
|
return NoContent, HTTPStatus.NO_CONTENT
|
|
@@ -23,6 +23,7 @@ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_
|
|
|
23
23
|
from airflow.configuration import conf
|
|
24
24
|
from airflow.providers.fab.auth_manager.api_fastapi.datamodels.login import LoginBody, LoginResponse
|
|
25
25
|
from airflow.providers.fab.auth_manager.api_fastapi.services.login import FABAuthManagerLogin
|
|
26
|
+
from airflow.providers.fab.auth_manager.cli_commands.utils import get_application_builder
|
|
26
27
|
|
|
27
28
|
login_router = AirflowRouter(tags=["FabAuthManager"])
|
|
28
29
|
|
|
@@ -35,7 +36,8 @@ login_router = AirflowRouter(tags=["FabAuthManager"])
|
|
|
35
36
|
)
|
|
36
37
|
def create_token(body: LoginBody) -> LoginResponse:
|
|
37
38
|
"""Generate a new API token."""
|
|
38
|
-
|
|
39
|
+
with get_application_builder():
|
|
40
|
+
return FABAuthManagerLogin.create_token(body=body)
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
@login_router.post(
|
|
@@ -46,6 +48,7 @@ def create_token(body: LoginBody) -> LoginResponse:
|
|
|
46
48
|
)
|
|
47
49
|
def create_token_cli(body: LoginBody) -> LoginResponse:
|
|
48
50
|
"""Generate a new CLI API token."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
with get_application_builder():
|
|
52
|
+
return FABAuthManagerLogin.create_token(
|
|
53
|
+
body=body, expiration_time_in_seconds=conf.getint("api_auth", "jwt_cli_expiration_time")
|
|
54
|
+
)
|
|
@@ -144,9 +144,8 @@ def users_delete(args):
|
|
|
144
144
|
@providers_configuration_loaded
|
|
145
145
|
def users_manage_role(args, remove=False):
|
|
146
146
|
"""Delete or appends user roles."""
|
|
147
|
-
user = _find_user(args)
|
|
148
|
-
|
|
149
147
|
with get_application_builder() as appbuilder:
|
|
148
|
+
user = _find_user(args)
|
|
150
149
|
role = appbuilder.sm.find_role(args.role)
|
|
151
150
|
if not role:
|
|
152
151
|
valid_roles = appbuilder.sm.get_all_roles()
|
|
@@ -25,6 +25,7 @@ from os.path import isabs
|
|
|
25
25
|
from typing import TYPE_CHECKING
|
|
26
26
|
|
|
27
27
|
from flask import Flask
|
|
28
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
28
29
|
from sqlalchemy.engine import make_url
|
|
29
30
|
|
|
30
31
|
import airflow
|
|
@@ -39,11 +40,11 @@ if TYPE_CHECKING:
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
@cache
|
|
42
|
-
def _return_appbuilder(app: Flask) -> AirflowAppBuilder:
|
|
43
|
+
def _return_appbuilder(app: Flask, db) -> AirflowAppBuilder:
|
|
43
44
|
"""Return an appbuilder instance for the given app."""
|
|
44
45
|
init_appbuilder(app, enable_plugins=False)
|
|
45
46
|
init_plugins(app)
|
|
46
|
-
init_airflow_session_interface(app)
|
|
47
|
+
init_airflow_session_interface(app, db)
|
|
47
48
|
return app.appbuilder # type: ignore[attr-defined]
|
|
48
49
|
|
|
49
50
|
|
|
@@ -63,4 +64,7 @@ def get_application_builder() -> Generator[AirflowAppBuilder, None, None]:
|
|
|
63
64
|
"Please use absolute path such as `sqlite:////tmp/airflow.db`."
|
|
64
65
|
)
|
|
65
66
|
flask_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
66
|
-
|
|
67
|
+
|
|
68
|
+
db = SQLAlchemy(flask_app)
|
|
69
|
+
yield _return_appbuilder(flask_app, db)
|
|
70
|
+
db.engine.dispose(close=True)
|
|
@@ -26,7 +26,7 @@ from urllib.parse import urljoin
|
|
|
26
26
|
import packaging.version
|
|
27
27
|
from connexion import FlaskApi
|
|
28
28
|
from fastapi import FastAPI
|
|
29
|
-
from flask import Blueprint, g
|
|
29
|
+
from flask import Blueprint, current_app, g
|
|
30
30
|
from sqlalchemy import select
|
|
31
31
|
from sqlalchemy.orm import Session, joinedload
|
|
32
32
|
from starlette.middleware.wsgi import WSGIMiddleware
|
|
@@ -282,7 +282,7 @@ class FabAuthManager(BaseAuthManager[User]):
|
|
|
282
282
|
|
|
283
283
|
def deserialize_user(self, token: dict[str, Any]) -> User:
|
|
284
284
|
with create_session() as session:
|
|
285
|
-
return session.
|
|
285
|
+
return session.scalars(select(User).where(User.id == int(token["sub"]))).one()
|
|
286
286
|
|
|
287
287
|
def serialize_user(self, user: User) -> dict[str, Any]:
|
|
288
288
|
return {"sub": str(user.id)}
|
|
@@ -292,7 +292,7 @@ class FabAuthManager(BaseAuthManager[User]):
|
|
|
292
292
|
user = self.get_user()
|
|
293
293
|
return (
|
|
294
294
|
self.appbuilder
|
|
295
|
-
and self.appbuilder.
|
|
295
|
+
and self.appbuilder.app.config.get("AUTH_ROLE_PUBLIC", None)
|
|
296
296
|
or (not user.is_anonymous and user.is_active)
|
|
297
297
|
)
|
|
298
298
|
|
|
@@ -470,14 +470,18 @@ class FabAuthManager(BaseAuthManager[User]):
|
|
|
470
470
|
return {dag.dag_id for dag in session.execute(select(DagModel.dag_id))}
|
|
471
471
|
if isinstance(user, AnonymousUser):
|
|
472
472
|
return set()
|
|
473
|
-
user_query =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
473
|
+
user_query = (
|
|
474
|
+
session.scalars(
|
|
475
|
+
select(User)
|
|
476
|
+
.options(
|
|
477
|
+
joinedload(User.roles)
|
|
478
|
+
.subqueryload(Role.permissions)
|
|
479
|
+
.options(joinedload(Permission.action), joinedload(Permission.resource))
|
|
480
|
+
)
|
|
481
|
+
.where(User.id == user.id)
|
|
479
482
|
)
|
|
480
|
-
.
|
|
483
|
+
.unique()
|
|
484
|
+
.one()
|
|
481
485
|
)
|
|
482
486
|
roles = user_query.roles
|
|
483
487
|
|
|
@@ -547,7 +551,7 @@ class FabAuthManager(BaseAuthManager[User]):
|
|
|
547
551
|
if not self.appbuilder:
|
|
548
552
|
raise AirflowException("AppBuilder is not initialized.")
|
|
549
553
|
|
|
550
|
-
sm_from_config =
|
|
554
|
+
sm_from_config = current_app.config.get("SECURITY_MANAGER_CLASS")
|
|
551
555
|
if sm_from_config:
|
|
552
556
|
if not issubclass(sm_from_config, FabAirflowSecurityManagerOverride):
|
|
553
557
|
raise AirflowConfigException(
|
|
@@ -23,9 +23,8 @@ import datetime
|
|
|
23
23
|
# Copyright 2013, Daniel Vaz Gaspar
|
|
24
24
|
from typing import TYPE_CHECKING
|
|
25
25
|
|
|
26
|
-
import packaging.version
|
|
27
26
|
from flask import current_app, g
|
|
28
|
-
from flask_appbuilder
|
|
27
|
+
from flask_appbuilder import Model
|
|
29
28
|
from sqlalchemy import (
|
|
30
29
|
Boolean,
|
|
31
30
|
Column,
|
|
@@ -33,7 +32,7 @@ from sqlalchemy import (
|
|
|
33
32
|
ForeignKey,
|
|
34
33
|
Index,
|
|
35
34
|
Integer,
|
|
36
|
-
|
|
35
|
+
Sequence,
|
|
37
36
|
String,
|
|
38
37
|
Table,
|
|
39
38
|
UniqueConstraint,
|
|
@@ -41,11 +40,19 @@ from sqlalchemy import (
|
|
|
41
40
|
func,
|
|
42
41
|
select,
|
|
43
42
|
)
|
|
44
|
-
from sqlalchemy.orm import backref, declared_attr,
|
|
43
|
+
from sqlalchemy.orm import Mapped, backref, declared_attr, relationship
|
|
45
44
|
|
|
46
|
-
from airflow import __version__ as airflow_version
|
|
47
45
|
from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
|
|
48
|
-
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from sqlalchemy.orm import mapped_column
|
|
49
|
+
except ImportError:
|
|
50
|
+
# fallback for SQLAlchemy < 2.0
|
|
51
|
+
def mapped_column(*args, **kwargs):
|
|
52
|
+
from sqlalchemy import Column
|
|
53
|
+
|
|
54
|
+
return Column(*args, **kwargs)
|
|
55
|
+
|
|
49
56
|
|
|
50
57
|
if TYPE_CHECKING:
|
|
51
58
|
try:
|
|
@@ -57,25 +64,88 @@ if TYPE_CHECKING:
|
|
|
57
64
|
Compatibility note: The models in this file are duplicated from Flask AppBuilder.
|
|
58
65
|
"""
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
assoc_group_role = Table(
|
|
68
|
+
"ab_group_role",
|
|
69
|
+
Model.metadata,
|
|
70
|
+
Column(
|
|
71
|
+
"id",
|
|
72
|
+
Integer,
|
|
73
|
+
Sequence("ab_group_role_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
74
|
+
primary_key=True,
|
|
75
|
+
),
|
|
76
|
+
Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
|
|
77
|
+
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
|
78
|
+
UniqueConstraint("group_id", "role_id"),
|
|
79
|
+
Index("idx_group_id", "group_id"),
|
|
80
|
+
Index("idx_group_role_id", "role_id"),
|
|
81
|
+
)
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
assoc_permission_role = Table(
|
|
84
|
+
"ab_permission_view_role",
|
|
85
|
+
Model.metadata,
|
|
86
|
+
Column(
|
|
87
|
+
"id",
|
|
88
|
+
Integer,
|
|
89
|
+
Sequence(
|
|
90
|
+
"ab_permission_view_role_id_seq",
|
|
91
|
+
start=1,
|
|
92
|
+
increment=1,
|
|
93
|
+
minvalue=1,
|
|
94
|
+
cycle=False,
|
|
95
|
+
),
|
|
96
|
+
primary_key=True,
|
|
97
|
+
),
|
|
98
|
+
Column(
|
|
99
|
+
"permission_view_id",
|
|
100
|
+
Integer,
|
|
101
|
+
ForeignKey("ab_permission_view.id", ondelete="CASCADE"),
|
|
102
|
+
),
|
|
103
|
+
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
|
104
|
+
UniqueConstraint("permission_view_id", "role_id"),
|
|
105
|
+
Index("idx_permission_view_id", "permission_view_id"),
|
|
106
|
+
Index("idx_role_id", "role_id"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assoc_user_role = Table(
|
|
110
|
+
"ab_user_role",
|
|
111
|
+
Model.metadata,
|
|
112
|
+
Column(
|
|
113
|
+
"id",
|
|
114
|
+
Integer,
|
|
115
|
+
Sequence("ab_user_role_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
116
|
+
primary_key=True,
|
|
117
|
+
),
|
|
118
|
+
Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
|
|
119
|
+
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
|
120
|
+
UniqueConstraint("user_id", "role_id"),
|
|
121
|
+
)
|
|
69
122
|
|
|
70
|
-
|
|
123
|
+
assoc_user_group = Table(
|
|
124
|
+
"ab_user_group",
|
|
125
|
+
Model.metadata,
|
|
126
|
+
Column(
|
|
127
|
+
"id",
|
|
128
|
+
Integer,
|
|
129
|
+
Sequence("ab_user_group_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
130
|
+
primary_key=True,
|
|
131
|
+
),
|
|
132
|
+
Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
|
|
133
|
+
Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
|
|
134
|
+
UniqueConstraint("user_id", "group_id"),
|
|
135
|
+
)
|
|
71
136
|
|
|
72
137
|
|
|
73
138
|
class Action(Model):
|
|
74
139
|
"""Represents permission actions such as `can_read`."""
|
|
75
140
|
|
|
76
141
|
__tablename__ = "ab_permission"
|
|
77
|
-
|
|
78
|
-
|
|
142
|
+
|
|
143
|
+
id: Mapped[int] = mapped_column(
|
|
144
|
+
Integer,
|
|
145
|
+
Sequence("ab_permission_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
146
|
+
primary_key=True,
|
|
147
|
+
)
|
|
148
|
+
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
|
79
149
|
|
|
80
150
|
def __repr__(self):
|
|
81
151
|
return self.name
|
|
@@ -85,8 +155,13 @@ class Resource(Model):
|
|
|
85
155
|
"""Represents permission object such as `User` or `Dag`."""
|
|
86
156
|
|
|
87
157
|
__tablename__ = "ab_view_menu"
|
|
88
|
-
|
|
89
|
-
|
|
158
|
+
|
|
159
|
+
id: Mapped[int] = mapped_column(
|
|
160
|
+
Integer,
|
|
161
|
+
Sequence("ab_view_menu_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
162
|
+
primary_key=True,
|
|
163
|
+
)
|
|
164
|
+
name: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
|
|
90
165
|
|
|
91
166
|
def __eq__(self, other):
|
|
92
167
|
return (isinstance(other, self.__class__)) and (self.name == other.name)
|
|
@@ -98,50 +173,18 @@ class Resource(Model):
|
|
|
98
173
|
return self.name
|
|
99
174
|
|
|
100
175
|
|
|
101
|
-
assoc_permission_role = Table(
|
|
102
|
-
"ab_permission_view_role",
|
|
103
|
-
Model.metadata,
|
|
104
|
-
Column("id", Integer, primary_key=True),
|
|
105
|
-
Column(
|
|
106
|
-
"permission_view_id",
|
|
107
|
-
Integer,
|
|
108
|
-
ForeignKey("ab_permission_view.id", ondelete="CASCADE"),
|
|
109
|
-
),
|
|
110
|
-
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
|
111
|
-
UniqueConstraint("permission_view_id", "role_id"),
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
assoc_user_group = Table(
|
|
115
|
-
"ab_user_group",
|
|
116
|
-
Model.metadata,
|
|
117
|
-
Column("id", Integer, primary_key=True),
|
|
118
|
-
Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
|
|
119
|
-
Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
|
|
120
|
-
UniqueConstraint("user_id", "group_id"),
|
|
121
|
-
Index("idx_user_id", "user_id"),
|
|
122
|
-
Index("idx_user_group_id", "group_id"),
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
assoc_group_role = Table(
|
|
126
|
-
"ab_group_role",
|
|
127
|
-
Model.metadata,
|
|
128
|
-
Column("id", Integer, primary_key=True),
|
|
129
|
-
Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
|
|
130
|
-
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
|
131
|
-
UniqueConstraint("group_id", "role_id"),
|
|
132
|
-
Index("idx_group_id", "group_id"),
|
|
133
|
-
Index("idx_group_role_id", "role_id"),
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
137
176
|
class Role(Model):
|
|
138
177
|
"""Represents a user role to which permissions can be assigned."""
|
|
139
178
|
|
|
140
179
|
__tablename__ = "ab_role"
|
|
141
180
|
|
|
142
|
-
id =
|
|
143
|
-
|
|
144
|
-
|
|
181
|
+
id: Mapped[int] = mapped_column(
|
|
182
|
+
Integer,
|
|
183
|
+
Sequence("ab_role_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
184
|
+
primary_key=True,
|
|
185
|
+
)
|
|
186
|
+
name: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
|
187
|
+
permissions: Mapped[list[Permission]] = relationship(
|
|
145
188
|
"Permission",
|
|
146
189
|
secondary=assoc_permission_role,
|
|
147
190
|
backref="role",
|
|
@@ -153,93 +196,100 @@ class Role(Model):
|
|
|
153
196
|
return self.name
|
|
154
197
|
|
|
155
198
|
|
|
156
|
-
class Group(Model):
|
|
157
|
-
"""Represents a user group."""
|
|
158
|
-
|
|
159
|
-
__tablename__ = "ab_group"
|
|
160
|
-
|
|
161
|
-
id = Column(Integer, primary_key=True)
|
|
162
|
-
name = Column(String(100), unique=True, nullable=False)
|
|
163
|
-
label = Column(String(150))
|
|
164
|
-
description = Column(String(512))
|
|
165
|
-
users = relationship("User", secondary=assoc_user_group, backref="groups", passive_deletes=True)
|
|
166
|
-
roles = relationship("Role", secondary=assoc_group_role, backref="groups", passive_deletes=True)
|
|
167
|
-
|
|
168
|
-
def __repr__(self):
|
|
169
|
-
return self.name
|
|
170
|
-
|
|
171
|
-
|
|
172
199
|
class Permission(Model):
|
|
173
200
|
"""Permission pair comprised of an Action + Resource combo."""
|
|
174
201
|
|
|
175
202
|
__tablename__ = "ab_permission_view"
|
|
176
203
|
__table_args__ = (UniqueConstraint("permission_id", "view_menu_id"),)
|
|
177
|
-
id =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
uselist=False,
|
|
182
|
-
lazy="joined",
|
|
183
|
-
)
|
|
184
|
-
resource_id = Column("view_menu_id", Integer, ForeignKey("ab_view_menu.id"))
|
|
185
|
-
resource = relationship(
|
|
186
|
-
"Resource",
|
|
187
|
-
uselist=False,
|
|
188
|
-
lazy="joined",
|
|
204
|
+
id: Mapped[int] = mapped_column(
|
|
205
|
+
Integer,
|
|
206
|
+
Sequence("ab_permission_view_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
207
|
+
primary_key=True,
|
|
189
208
|
)
|
|
209
|
+
action_id: Mapped[int] = mapped_column("permission_id", Integer, ForeignKey("ab_permission.id"))
|
|
210
|
+
action: Mapped[Action] = relationship("Action", lazy="joined", uselist=False)
|
|
211
|
+
resource_id: Mapped[int] = mapped_column("view_menu_id", Integer, ForeignKey("ab_view_menu.id"))
|
|
212
|
+
resource: Mapped[Resource] = relationship("Resource", lazy="joined", uselist=False)
|
|
190
213
|
|
|
191
214
|
def __repr__(self):
|
|
192
|
-
return str(self.action).replace("_", " ") + " on
|
|
215
|
+
return str(self.action).replace("_", " ") + f" on {str(self.resource)}"
|
|
193
216
|
|
|
194
217
|
|
|
195
|
-
|
|
196
|
-
"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
218
|
+
class Group(Model):
|
|
219
|
+
"""Represents an Airflow user group."""
|
|
220
|
+
|
|
221
|
+
__tablename__ = "ab_group"
|
|
222
|
+
|
|
223
|
+
id: Mapped[int] = mapped_column(
|
|
224
|
+
Integer,
|
|
225
|
+
Sequence("ab_group_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
226
|
+
primary_key=True,
|
|
227
|
+
)
|
|
228
|
+
name: Mapped[str] = Column(String(100), unique=True, nullable=False)
|
|
229
|
+
label: Mapped[str] = Column(String(150))
|
|
230
|
+
description: Mapped[str] = Column(String(512))
|
|
231
|
+
users: Mapped[list[User]] = relationship(
|
|
232
|
+
"User", secondary=assoc_user_group, backref="groups", passive_deletes=True
|
|
233
|
+
)
|
|
234
|
+
roles: Mapped[list[Role]] = relationship(
|
|
235
|
+
"Role", secondary=assoc_group_role, backref="groups", passive_deletes=True
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def __repr__(self):
|
|
239
|
+
return self.name
|
|
203
240
|
|
|
204
241
|
|
|
205
242
|
class User(Model, BaseUser):
|
|
206
243
|
"""Represents an Airflow user which has roles assigned to it."""
|
|
207
244
|
|
|
208
245
|
__tablename__ = "ab_user"
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
246
|
+
|
|
247
|
+
id: Mapped[int] = mapped_column(
|
|
248
|
+
Integer,
|
|
249
|
+
Sequence("ab_user_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
250
|
+
primary_key=True,
|
|
251
|
+
)
|
|
252
|
+
first_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
253
|
+
last_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
254
|
+
username: Mapped[str] = mapped_column(
|
|
213
255
|
String(512).with_variant(String(512, collation="NOCASE"), "sqlite"), unique=True, nullable=False
|
|
214
256
|
)
|
|
215
|
-
password =
|
|
216
|
-
active =
|
|
217
|
-
email =
|
|
218
|
-
last_login =
|
|
219
|
-
login_count =
|
|
220
|
-
fail_login_count =
|
|
221
|
-
roles = relationship(
|
|
222
|
-
"Role",
|
|
257
|
+
password: Mapped[str | None] = mapped_column(String(256))
|
|
258
|
+
active: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
|
259
|
+
email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False)
|
|
260
|
+
last_login: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True)
|
|
261
|
+
login_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
262
|
+
fail_login_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
263
|
+
roles: Mapped[list[Role]] = relationship(
|
|
264
|
+
"Role",
|
|
265
|
+
secondary=assoc_user_role,
|
|
266
|
+
backref="user",
|
|
267
|
+
lazy="selectin",
|
|
268
|
+
passive_deletes=True,
|
|
269
|
+
)
|
|
270
|
+
created_on: Mapped[datetime.datetime | None] = mapped_column(
|
|
271
|
+
DateTime, default=lambda: datetime.datetime.now(), nullable=True
|
|
272
|
+
)
|
|
273
|
+
changed_on: Mapped[datetime.datetime | None] = mapped_column(
|
|
274
|
+
DateTime, default=lambda: datetime.datetime.now(), nullable=True
|
|
223
275
|
)
|
|
224
|
-
created_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
|
|
225
|
-
changed_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
|
|
226
276
|
|
|
227
277
|
@declared_attr
|
|
228
|
-
def created_by_fk(self):
|
|
278
|
+
def created_by_fk(self) -> Column:
|
|
229
279
|
return Column(Integer, ForeignKey("ab_user.id"), default=self.get_user_id, nullable=True)
|
|
230
280
|
|
|
231
281
|
@declared_attr
|
|
232
|
-
def changed_by_fk(self):
|
|
282
|
+
def changed_by_fk(self) -> Column:
|
|
233
283
|
return Column(Integer, ForeignKey("ab_user.id"), default=self.get_user_id, nullable=True)
|
|
234
284
|
|
|
235
|
-
created_by = relationship(
|
|
285
|
+
created_by: Mapped[User] = relationship(
|
|
236
286
|
"User",
|
|
237
287
|
backref=backref("created", uselist=True),
|
|
238
288
|
remote_side=[id],
|
|
239
289
|
primaryjoin="User.created_by_fk == User.id",
|
|
240
290
|
uselist=False,
|
|
241
291
|
)
|
|
242
|
-
changed_by = relationship(
|
|
292
|
+
changed_by: Mapped[User] = relationship(
|
|
243
293
|
"User",
|
|
244
294
|
backref=backref("changed", uselist=True),
|
|
245
295
|
remote_side=[id],
|
|
@@ -274,7 +324,7 @@ class User(Model, BaseUser):
|
|
|
274
324
|
if current_app:
|
|
275
325
|
sm = current_app.appbuilder.sm
|
|
276
326
|
self._perms: set[tuple[str, str]] = set(
|
|
277
|
-
sm.
|
|
327
|
+
sm.session.execute(
|
|
278
328
|
select(sm.action_model.name, sm.resource_model.name)
|
|
279
329
|
.join(sm.permission_model.action)
|
|
280
330
|
.join(sm.permission_model.resource)
|
|
@@ -307,16 +357,23 @@ class RegisterUser(Model):
|
|
|
307
357
|
"""Represents a user registration."""
|
|
308
358
|
|
|
309
359
|
__tablename__ = "ab_register_user"
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
360
|
+
|
|
361
|
+
id = mapped_column(
|
|
362
|
+
Integer,
|
|
363
|
+
Sequence("ab_register_user_id_seq", start=1, increment=1, minvalue=1, cycle=False),
|
|
364
|
+
primary_key=True,
|
|
365
|
+
)
|
|
366
|
+
first_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
367
|
+
last_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
368
|
+
username: Mapped[str] = mapped_column(
|
|
314
369
|
String(512).with_variant(String(512, collation="NOCASE"), "sqlite"), unique=True, nullable=False
|
|
315
370
|
)
|
|
316
|
-
password =
|
|
317
|
-
email =
|
|
318
|
-
registration_date
|
|
319
|
-
|
|
371
|
+
password: Mapped[str | None] = mapped_column(String(256))
|
|
372
|
+
email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False)
|
|
373
|
+
registration_date: Mapped[datetime.datetime | None] = mapped_column(
|
|
374
|
+
DateTime, default=lambda: datetime.datetime.now(), nullable=True
|
|
375
|
+
)
|
|
376
|
+
registration_hash: Mapped[str | None] = mapped_column(String(256))
|
|
320
377
|
|
|
321
378
|
|
|
322
379
|
@event.listens_for(User.__table__, "before_create")
|
|
@@ -18,9 +18,10 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
from flask_appbuilder import Model
|
|
22
|
+
|
|
21
23
|
from airflow import settings
|
|
22
24
|
from airflow.exceptions import AirflowException
|
|
23
|
-
from airflow.providers.fab.auth_manager.models import metadata
|
|
24
25
|
from airflow.utils.db import _offline_migration, print_happy_cat
|
|
25
26
|
from airflow.utils.db_manager import BaseDBManager
|
|
26
27
|
|
|
@@ -41,14 +42,14 @@ def _get_flask_db(sql_database_uri):
|
|
|
41
42
|
flask_app.config["SQLALCHEMY_DATABASE_URI"] = sql_database_uri
|
|
42
43
|
flask_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
43
44
|
db = SQLAlchemy(flask_app)
|
|
44
|
-
AirflowDatabaseSessionInterface(app=flask_app,
|
|
45
|
-
return db
|
|
45
|
+
AirflowDatabaseSessionInterface(app=flask_app, client=db, table="session", key_prefix="")
|
|
46
|
+
return db, flask_app
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
class FABDBManager(BaseDBManager):
|
|
49
50
|
"""Manages FAB database."""
|
|
50
51
|
|
|
51
|
-
metadata = metadata
|
|
52
|
+
metadata = Model.metadata
|
|
52
53
|
version_table_name = "alembic_version_fab"
|
|
53
54
|
migration_dir = (PACKAGE_DIR / "migrations").as_posix()
|
|
54
55
|
alembic_file = (PACKAGE_DIR / "alembic.ini").as_posix()
|
|
@@ -57,7 +58,9 @@ class FABDBManager(BaseDBManager):
|
|
|
57
58
|
|
|
58
59
|
def create_db_from_orm(self):
|
|
59
60
|
super().create_db_from_orm()
|
|
60
|
-
_get_flask_db(settings.SQL_ALCHEMY_CONN)
|
|
61
|
+
db, flask_app = _get_flask_db(settings.SQL_ALCHEMY_CONN)
|
|
62
|
+
with flask_app.app_context():
|
|
63
|
+
db.create_all()
|
|
61
64
|
|
|
62
65
|
def reset_to_2_x(self):
|
|
63
66
|
self.create_db_from_orm()
|
|
@@ -126,4 +129,6 @@ class FABDBManager(BaseDBManager):
|
|
|
126
129
|
|
|
127
130
|
def drop_tables(self, connection):
|
|
128
131
|
super().drop_tables(connection)
|
|
129
|
-
_get_flask_db(settings.SQL_ALCHEMY_CONN)
|
|
132
|
+
db, flask_app = _get_flask_db(settings.SQL_ALCHEMY_CONN)
|
|
133
|
+
with flask_app.app_context():
|
|
134
|
+
db.drop_all()
|