apache-airflow-providers-fab 1.3.0rc1__py3-none-any.whl → 1.4.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/alembic.ini +133 -0
- airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +4 -2
- airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +110 -8
- airflow/providers/fab/auth_manager/cli_commands/db_command.py +52 -0
- airflow/providers/fab/auth_manager/cli_commands/definition.py +61 -0
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +6 -5
- airflow/providers/fab/auth_manager/fab_auth_manager.py +30 -6
- airflow/providers/fab/auth_manager/models/__init__.py +21 -8
- airflow/providers/fab/auth_manager/models/anonymous_user.py +7 -1
- airflow/providers/fab/auth_manager/models/db.py +107 -0
- airflow/providers/fab/auth_manager/security_manager/override.py +5 -3
- airflow/providers/fab/get_provider_info.py +4 -1
- airflow/providers/fab/migrations/README +1 -0
- airflow/providers/fab/migrations/__init__.py +16 -0
- airflow/providers/fab/migrations/env.py +125 -0
- airflow/providers/fab/migrations/script.py.mako +45 -0
- airflow/providers/fab/migrations/versions/0001_1_4_0_placeholder_migration.py +45 -0
- airflow/providers/fab/migrations/versions/__init__.py +16 -0
- {apache_airflow_providers_fab-1.3.0rc1.dist-info → apache_airflow_providers_fab-1.4.0rc1.dist-info}/METADATA +8 -6
- {apache_airflow_providers_fab-1.3.0rc1.dist-info → apache_airflow_providers_fab-1.4.0rc1.dist-info}/RECORD +23 -14
- {apache_airflow_providers_fab-1.3.0rc1.dist-info → apache_airflow_providers_fab-1.4.0rc1.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_fab-1.3.0rc1.dist-info → apache_airflow_providers_fab-1.4.0rc1.dist-info}/entry_points.txt +0 -0
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
|
|
29
29
|
|
30
30
|
__all__ = ["__version__"]
|
31
31
|
|
32
|
-
__version__ = "1.
|
32
|
+
__version__ = "1.4.0"
|
33
33
|
|
34
34
|
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
|
35
35
|
"2.9.0"
|
@@ -0,0 +1,133 @@
|
|
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
|
+
# A generic, single database configuration.
|
19
|
+
|
20
|
+
[alembic]
|
21
|
+
# path to migration scripts
|
22
|
+
# Use forward slashes (/) also on windows to provide an os agnostic path
|
23
|
+
script_location = %(here)s/migrations
|
24
|
+
|
25
|
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
26
|
+
# Uncomment the line below if you want the files to be prepended with date and time
|
27
|
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
28
|
+
# for all available tokens
|
29
|
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
30
|
+
|
31
|
+
# sys.path path, will be prepended to sys.path if present.
|
32
|
+
# defaults to the current working directory.
|
33
|
+
prepend_sys_path = .
|
34
|
+
|
35
|
+
# timezone to use when rendering the date within the migration file
|
36
|
+
# as well as the filename.
|
37
|
+
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
38
|
+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
39
|
+
# string value is passed to ZoneInfo()
|
40
|
+
# leave blank for localtime
|
41
|
+
# timezone =
|
42
|
+
|
43
|
+
# max length of characters to apply to the "slug" field
|
44
|
+
# truncate_slug_length = 40
|
45
|
+
|
46
|
+
# set to 'true' to run the environment during
|
47
|
+
# the 'revision' command, regardless of autogenerate
|
48
|
+
# revision_environment = false
|
49
|
+
|
50
|
+
# set to 'true' to allow .pyc and .pyo files without
|
51
|
+
# a source .py file to be detected as revisions in the
|
52
|
+
# versions/ directory
|
53
|
+
# sourceless = false
|
54
|
+
|
55
|
+
# version location specification; This defaults
|
56
|
+
# to alembic/versions. When using multiple version
|
57
|
+
# directories, initial revisions must be specified with --version-path.
|
58
|
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
59
|
+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
60
|
+
|
61
|
+
# version path separator; As mentioned above, this is the character used to split
|
62
|
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
63
|
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
64
|
+
# Valid values for version_path_separator are:
|
65
|
+
#
|
66
|
+
# version_path_separator = :
|
67
|
+
# version_path_separator = ;
|
68
|
+
# version_path_separator = space
|
69
|
+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
70
|
+
|
71
|
+
# set to 'true' to search source files recursively
|
72
|
+
# in each "version_locations" directory
|
73
|
+
# new in Alembic version 1.10
|
74
|
+
# recursive_version_locations = false
|
75
|
+
|
76
|
+
# the output encoding used when revision files
|
77
|
+
# are written from script.py.mako
|
78
|
+
# output_encoding = utf-8
|
79
|
+
|
80
|
+
sqlalchemy.url = scheme://localhost/airflow
|
81
|
+
|
82
|
+
|
83
|
+
[post_write_hooks]
|
84
|
+
# post_write_hooks defines scripts or Python functions that are run
|
85
|
+
# on newly generated revision scripts. See the documentation for further
|
86
|
+
# detail and examples
|
87
|
+
|
88
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
89
|
+
# hooks = black
|
90
|
+
# black.type = console_scripts
|
91
|
+
# black.entrypoint = black
|
92
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
93
|
+
|
94
|
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
95
|
+
# hooks = ruff
|
96
|
+
# ruff.type = exec
|
97
|
+
# ruff.executable = %(here)s/.venv/bin/ruff
|
98
|
+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
99
|
+
|
100
|
+
# Logging configuration
|
101
|
+
[loggers]
|
102
|
+
keys = root,sqlalchemy,alembic
|
103
|
+
|
104
|
+
[handlers]
|
105
|
+
keys = console
|
106
|
+
|
107
|
+
[formatters]
|
108
|
+
keys = generic
|
109
|
+
|
110
|
+
[logger_root]
|
111
|
+
level = WARN
|
112
|
+
handlers = console
|
113
|
+
qualname =
|
114
|
+
|
115
|
+
[logger_sqlalchemy]
|
116
|
+
level = WARN
|
117
|
+
handlers =
|
118
|
+
qualname = sqlalchemy.engine
|
119
|
+
|
120
|
+
[logger_alembic]
|
121
|
+
level = INFO
|
122
|
+
handlers =
|
123
|
+
qualname = alembic
|
124
|
+
|
125
|
+
[handler_console]
|
126
|
+
class = StreamHandler
|
127
|
+
args = (sys.stderr,)
|
128
|
+
level = NOTSET
|
129
|
+
formatter = generic
|
130
|
+
|
131
|
+
[formatter_generic]
|
132
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
133
|
+
datefmt = %H:%M:%S
|
@@ -21,7 +21,7 @@ from __future__ import annotations
|
|
21
21
|
from functools import wraps
|
22
22
|
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
|
23
23
|
|
24
|
-
from flask import Response, request
|
24
|
+
from flask import Response, current_app, request
|
25
25
|
from flask_appbuilder.const import AUTH_LDAP
|
26
26
|
from flask_login import login_user
|
27
27
|
|
@@ -62,7 +62,9 @@ def requires_authentication(function: T):
|
|
62
62
|
|
63
63
|
@wraps(function)
|
64
64
|
def decorated(*args, **kwargs):
|
65
|
-
if auth_current_user() is not None
|
65
|
+
if auth_current_user() is not None or current_app.appbuilder.get_app.config.get(
|
66
|
+
"AUTH_ROLE_PUBLIC", None
|
67
|
+
):
|
66
68
|
return function(*args, **kwargs)
|
67
69
|
else:
|
68
70
|
return Response("Unauthorized", 401, {"WWW-Authenticate": "Basic"})
|
@@ -18,27 +18,129 @@
|
|
18
18
|
from __future__ import annotations
|
19
19
|
|
20
20
|
import logging
|
21
|
-
|
22
|
-
from
|
21
|
+
import os
|
22
|
+
from functools import wraps
|
23
|
+
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, cast
|
23
24
|
|
25
|
+
import kerberos
|
26
|
+
from flask import Response, current_app, g, make_response, request
|
24
27
|
from requests_kerberos import HTTPKerberosAuth
|
25
28
|
|
26
|
-
from airflow.
|
27
|
-
init_app as base_init_app,
|
28
|
-
requires_authentication as base_requires_authentication,
|
29
|
-
)
|
29
|
+
from airflow.configuration import conf
|
30
30
|
from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
|
31
|
+
from airflow.utils.net import getfqdn
|
31
32
|
from airflow.www.extensions.init_auth_manager import get_auth_manager
|
32
33
|
|
34
|
+
if TYPE_CHECKING:
|
35
|
+
from airflow.auth.managers.models.base_user import BaseUser
|
36
|
+
|
33
37
|
log = logging.getLogger(__name__)
|
34
38
|
|
35
39
|
CLIENT_AUTH: tuple[str, str] | Any | None = HTTPKerberosAuth(service="airflow")
|
36
40
|
|
37
41
|
|
42
|
+
class KerberosService:
|
43
|
+
"""Class to keep information about the Kerberos Service initialized."""
|
44
|
+
|
45
|
+
def __init__(self):
|
46
|
+
self.service_name = None
|
47
|
+
|
48
|
+
|
49
|
+
class _KerberosAuth(NamedTuple):
|
50
|
+
return_code: int | None
|
51
|
+
user: str = ""
|
52
|
+
token: str | None = None
|
53
|
+
|
54
|
+
|
55
|
+
# Stores currently initialized Kerberos Service
|
56
|
+
_KERBEROS_SERVICE = KerberosService()
|
57
|
+
|
58
|
+
|
59
|
+
def init_app(app):
|
60
|
+
"""Initialize application with kerberos."""
|
61
|
+
hostname = app.config.get("SERVER_NAME")
|
62
|
+
if not hostname:
|
63
|
+
hostname = getfqdn()
|
64
|
+
log.info("Kerberos: hostname %s", hostname)
|
65
|
+
|
66
|
+
service = "airflow"
|
67
|
+
|
68
|
+
_KERBEROS_SERVICE.service_name = f"{service}@{hostname}"
|
69
|
+
|
70
|
+
if "KRB5_KTNAME" not in os.environ:
|
71
|
+
os.environ["KRB5_KTNAME"] = conf.get("kerberos", "keytab")
|
72
|
+
|
73
|
+
try:
|
74
|
+
log.info("Kerberos init: %s %s", service, hostname)
|
75
|
+
principal = kerberos.getServerPrincipalDetails(service, hostname)
|
76
|
+
except kerberos.KrbError as err:
|
77
|
+
log.warning("Kerberos: %s", err)
|
78
|
+
else:
|
79
|
+
log.info("Kerberos API: server is %s", principal)
|
80
|
+
|
81
|
+
|
82
|
+
def _unauthorized():
|
83
|
+
"""Indicate that authorization is required."""
|
84
|
+
return Response("Unauthorized", 401, {"WWW-Authenticate": "Negotiate"})
|
85
|
+
|
86
|
+
|
87
|
+
def _forbidden():
|
88
|
+
return Response("Forbidden", 403)
|
89
|
+
|
90
|
+
|
91
|
+
def _gssapi_authenticate(token) -> _KerberosAuth | None:
|
92
|
+
state = None
|
93
|
+
try:
|
94
|
+
return_code, state = kerberos.authGSSServerInit(_KERBEROS_SERVICE.service_name)
|
95
|
+
if return_code != kerberos.AUTH_GSS_COMPLETE:
|
96
|
+
return _KerberosAuth(return_code=None)
|
97
|
+
|
98
|
+
if (return_code := kerberos.authGSSServerStep(state, token)) == kerberos.AUTH_GSS_COMPLETE:
|
99
|
+
return _KerberosAuth(
|
100
|
+
return_code=return_code,
|
101
|
+
user=kerberos.authGSSServerUserName(state),
|
102
|
+
token=kerberos.authGSSServerResponse(state),
|
103
|
+
)
|
104
|
+
elif return_code == kerberos.AUTH_GSS_CONTINUE:
|
105
|
+
return _KerberosAuth(return_code=return_code)
|
106
|
+
return _KerberosAuth(return_code=return_code)
|
107
|
+
except kerberos.GSSError:
|
108
|
+
return _KerberosAuth(return_code=None)
|
109
|
+
finally:
|
110
|
+
if state:
|
111
|
+
kerberos.authGSSServerClean(state)
|
112
|
+
|
113
|
+
|
114
|
+
T = TypeVar("T", bound=Callable)
|
115
|
+
|
116
|
+
|
38
117
|
def find_user(username=None, email=None):
|
39
118
|
security_manager = cast(FabAirflowSecurityManagerOverride, get_auth_manager().security_manager)
|
40
119
|
return security_manager.find_user(username=username, email=email)
|
41
120
|
|
42
121
|
|
43
|
-
|
44
|
-
|
122
|
+
def requires_authentication(function: T, find_user: Callable[[str], BaseUser] | None = find_user):
|
123
|
+
"""Decorate functions that require authentication with Kerberos."""
|
124
|
+
|
125
|
+
@wraps(function)
|
126
|
+
def decorated(*args, **kwargs):
|
127
|
+
if current_app.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None):
|
128
|
+
response = function(*args, **kwargs)
|
129
|
+
return make_response(response)
|
130
|
+
|
131
|
+
header = request.headers.get("Authorization")
|
132
|
+
if header:
|
133
|
+
token = "".join(header.split()[1:])
|
134
|
+
auth = _gssapi_authenticate(token)
|
135
|
+
if auth.return_code == kerberos.AUTH_GSS_COMPLETE:
|
136
|
+
g.user = find_user(auth.user)
|
137
|
+
response = function(*args, **kwargs)
|
138
|
+
response = make_response(response)
|
139
|
+
if auth.token is not None:
|
140
|
+
response.headers["WWW-Authenticate"] = f"negotiate {auth.token}"
|
141
|
+
return response
|
142
|
+
elif auth.return_code != kerberos.AUTH_GSS_CONTINUE:
|
143
|
+
return _forbidden()
|
144
|
+
return _unauthorized()
|
145
|
+
|
146
|
+
return cast(T, decorated)
|
@@ -0,0 +1,52 @@
|
|
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 airflow import settings
|
20
|
+
from airflow.cli.commands.db_command import run_db_downgrade_command, run_db_migrate_command
|
21
|
+
from airflow.providers.fab.auth_manager.models.db import _REVISION_HEADS_MAP, FABDBManager
|
22
|
+
from airflow.utils import cli as cli_utils
|
23
|
+
from airflow.utils.providers_configuration_loader import providers_configuration_loaded
|
24
|
+
|
25
|
+
|
26
|
+
@providers_configuration_loaded
|
27
|
+
def resetdb(args):
|
28
|
+
"""Reset the metadata database."""
|
29
|
+
print(f"DB: {settings.engine.url!r}")
|
30
|
+
if not (args.yes or input("This will drop existing tables if they exist. Proceed? (y/n)").upper() == "Y"):
|
31
|
+
raise SystemExit("Cancelled")
|
32
|
+
FABDBManager(settings.Session()).resetdb(skip_init=args.skip_init)
|
33
|
+
|
34
|
+
|
35
|
+
@cli_utils.action_cli(check_db=False)
|
36
|
+
@providers_configuration_loaded
|
37
|
+
def migratedb(args):
|
38
|
+
"""Migrates the metadata database."""
|
39
|
+
session = settings.Session()
|
40
|
+
upgrade_command = FABDBManager(session).upgradedb
|
41
|
+
run_db_migrate_command(
|
42
|
+
args, upgrade_command, revision_heads_map=_REVISION_HEADS_MAP, reserialize_dags=False
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
@cli_utils.action_cli(check_db=False)
|
47
|
+
@providers_configuration_loaded
|
48
|
+
def downgrade(args):
|
49
|
+
"""Downgrades the metadata database."""
|
50
|
+
session = settings.Session()
|
51
|
+
dwongrade_command = FABDBManager(session).downgrade
|
52
|
+
run_db_downgrade_command(args, dwongrade_command, revision_heads_map=_REVISION_HEADS_MAP)
|
@@ -19,8 +19,17 @@ from __future__ import annotations
|
|
19
19
|
import textwrap
|
20
20
|
|
21
21
|
from airflow.cli.cli_config import (
|
22
|
+
ARG_DB_FROM_REVISION,
|
23
|
+
ARG_DB_FROM_VERSION,
|
24
|
+
ARG_DB_REVISION__DOWNGRADE,
|
25
|
+
ARG_DB_REVISION__UPGRADE,
|
26
|
+
ARG_DB_SKIP_INIT,
|
27
|
+
ARG_DB_SQL_ONLY,
|
28
|
+
ARG_DB_VERSION__DOWNGRADE,
|
29
|
+
ARG_DB_VERSION__UPGRADE,
|
22
30
|
ARG_OUTPUT,
|
23
31
|
ARG_VERBOSE,
|
32
|
+
ARG_YES,
|
24
33
|
ActionCommand,
|
25
34
|
Arg,
|
26
35
|
lazy_load_command,
|
@@ -243,3 +252,55 @@ SYNC_PERM_COMMAND = ActionCommand(
|
|
243
252
|
func=lazy_load_command("airflow.providers.fab.auth_manager.cli_commands.sync_perm_command.sync_perm"),
|
244
253
|
args=(ARG_INCLUDE_DAGS, ARG_VERBOSE),
|
245
254
|
)
|
255
|
+
|
256
|
+
DB_COMMANDS = (
|
257
|
+
ActionCommand(
|
258
|
+
name="migrate",
|
259
|
+
help="Migrates the FAB metadata database to the latest version",
|
260
|
+
description=(
|
261
|
+
"Migrate the schema of the FAB metadata database. "
|
262
|
+
"Create the database if it does not exist "
|
263
|
+
"To print but not execute commands, use option ``--show-sql-only``. "
|
264
|
+
"If using options ``--from-revision`` or ``--from-version``, you must also use "
|
265
|
+
"``--show-sql-only``, because if actually *running* migrations, we should only "
|
266
|
+
"migrate from the *current* Alembic revision."
|
267
|
+
),
|
268
|
+
func=lazy_load_command("airflow.providers.fab.auth_manager.cli_commands.db_command.migratedb"),
|
269
|
+
args=(
|
270
|
+
ARG_DB_REVISION__UPGRADE,
|
271
|
+
ARG_DB_VERSION__UPGRADE,
|
272
|
+
ARG_DB_SQL_ONLY,
|
273
|
+
ARG_DB_FROM_REVISION,
|
274
|
+
ARG_DB_FROM_VERSION,
|
275
|
+
ARG_VERBOSE,
|
276
|
+
),
|
277
|
+
),
|
278
|
+
ActionCommand(
|
279
|
+
name="downgrade",
|
280
|
+
help="Downgrade the schema of the FAB metadata database.",
|
281
|
+
description=(
|
282
|
+
"Downgrade the schema of the FAB metadata database. "
|
283
|
+
"You must provide either `--to-revision` or `--to-version`. "
|
284
|
+
"To print but not execute commands, use option `--show-sql-only`. "
|
285
|
+
"If using options `--from-revision` or `--from-version`, you must also use `--show-sql-only`, "
|
286
|
+
"because if actually *running* migrations, we should only migrate from the *current* Alembic "
|
287
|
+
"revision."
|
288
|
+
),
|
289
|
+
func=lazy_load_command("airflow.providers.fab.auth_manager.cli_commands.db_command.downgrade"),
|
290
|
+
args=(
|
291
|
+
ARG_DB_REVISION__DOWNGRADE,
|
292
|
+
ARG_DB_VERSION__DOWNGRADE,
|
293
|
+
ARG_DB_SQL_ONLY,
|
294
|
+
ARG_YES,
|
295
|
+
ARG_DB_FROM_REVISION,
|
296
|
+
ARG_DB_FROM_VERSION,
|
297
|
+
ARG_VERBOSE,
|
298
|
+
),
|
299
|
+
),
|
300
|
+
ActionCommand(
|
301
|
+
name="reset",
|
302
|
+
help="Burn down and rebuild the FAB metadata database",
|
303
|
+
func=lazy_load_command("airflow.providers.fab.auth_manager.cli_commands.db_command.resetdb"),
|
304
|
+
args=(ARG_YES, ARG_DB_SKIP_INIT, ARG_VERBOSE),
|
305
|
+
),
|
306
|
+
)
|
@@ -212,10 +212,12 @@ def users_import(args):
|
|
212
212
|
|
213
213
|
users_created, users_updated = _import_users(users_list)
|
214
214
|
if users_created:
|
215
|
-
|
215
|
+
users_created_str = "\n\t".join(users_created)
|
216
|
+
print(f"Created the following users:\n\t{users_created_str}")
|
216
217
|
|
217
218
|
if users_updated:
|
218
|
-
|
219
|
+
users_updated_str = "\n\t".join(users_updated)
|
220
|
+
print(f"Updated the following users:\n\t{users_updated_str}")
|
219
221
|
|
220
222
|
|
221
223
|
def _import_users(users_list: list[dict[str, Any]]):
|
@@ -231,9 +233,8 @@ def _import_users(users_list: list[dict[str, Any]]):
|
|
231
233
|
msg.append(f"[Item {row_num}]")
|
232
234
|
for key, value in failure.items():
|
233
235
|
msg.append(f"\t{key}: {value}")
|
234
|
-
|
235
|
-
|
236
|
-
)
|
236
|
+
msg_str = "\n".join(msg)
|
237
|
+
raise SystemExit(f"Error: Input file didn't pass validation. See below:\n{msg_str}")
|
237
238
|
|
238
239
|
for user in users_list:
|
239
240
|
roles = []
|
@@ -22,11 +22,14 @@ from functools import cached_property
|
|
22
22
|
from pathlib import Path
|
23
23
|
from typing import TYPE_CHECKING, Container
|
24
24
|
|
25
|
+
import packaging.version
|
25
26
|
from connexion import FlaskApi
|
26
27
|
from flask import Blueprint, url_for
|
28
|
+
from packaging.version import Version
|
27
29
|
from sqlalchemy import select
|
28
30
|
from sqlalchemy.orm import Session, joinedload
|
29
31
|
|
32
|
+
from airflow import __version__ as airflow_version
|
30
33
|
from airflow.auth.managers.base_auth_manager import BaseAuthManager, ResourceMethod
|
31
34
|
from airflow.auth.managers.models.resource_details import (
|
32
35
|
AccessView,
|
@@ -47,6 +50,7 @@ from airflow.configuration import conf
|
|
47
50
|
from airflow.exceptions import AirflowConfigException, AirflowException
|
48
51
|
from airflow.models import DagModel
|
49
52
|
from airflow.providers.fab.auth_manager.cli_commands.definition import (
|
53
|
+
DB_COMMANDS,
|
50
54
|
ROLES_COMMANDS,
|
51
55
|
SYNC_PERM_COMMAND,
|
52
56
|
USERS_COMMANDS,
|
@@ -81,6 +85,7 @@ from airflow.security.permissions import (
|
|
81
85
|
)
|
82
86
|
from airflow.utils.session import NEW_SESSION, provide_session
|
83
87
|
from airflow.utils.yaml import safe_load
|
88
|
+
from airflow.version import version
|
84
89
|
from airflow.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED
|
85
90
|
from airflow.www.extensions.init_views import _CustomErrorRequestBodyValidator, _LazyResolver
|
86
91
|
|
@@ -132,7 +137,7 @@ class FabAuthManager(BaseAuthManager):
|
|
132
137
|
@staticmethod
|
133
138
|
def get_cli_commands() -> list[CLICommand]:
|
134
139
|
"""Vends CLI commands to be included in Airflow CLI."""
|
135
|
-
|
140
|
+
commands: list[CLICommand] = [
|
136
141
|
GroupCommand(
|
137
142
|
name="users",
|
138
143
|
help="Manage users",
|
@@ -145,6 +150,12 @@ class FabAuthManager(BaseAuthManager):
|
|
145
150
|
),
|
146
151
|
SYNC_PERM_COMMAND, # not in a command group
|
147
152
|
]
|
153
|
+
# If Airflow version is 3.0.0 or higher, add the fab-db command group
|
154
|
+
if packaging.version.parse(
|
155
|
+
packaging.version.parse(airflow_version).base_version
|
156
|
+
) >= packaging.version.parse("3.0.0"):
|
157
|
+
commands.append(GroupCommand(name="fab-db", help="Manage FAB", subcommands=DB_COMMANDS))
|
158
|
+
return commands
|
148
159
|
|
149
160
|
def get_api_endpoints(self) -> None | Blueprint:
|
150
161
|
folder = Path(__file__).parents[0].resolve() # this is airflow/auth/managers/fab/
|
@@ -179,7 +190,13 @@ class FabAuthManager(BaseAuthManager):
|
|
179
190
|
|
180
191
|
def is_logged_in(self) -> bool:
|
181
192
|
"""Return whether the user is logged in."""
|
182
|
-
|
193
|
+
user = self.get_user()
|
194
|
+
if Version(Version(version).base_version) < Version("3.0.0"):
|
195
|
+
return not user.is_anonymous and user.is_active
|
196
|
+
else:
|
197
|
+
return self.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None) or (
|
198
|
+
not user.is_anonymous and user.is_active
|
199
|
+
)
|
183
200
|
|
184
201
|
def is_authorized_configuration(
|
185
202
|
self,
|
@@ -364,10 +381,15 @@ class FabAuthManager(BaseAuthManager):
|
|
364
381
|
|
365
382
|
def get_url_user_profile(self) -> str | None:
|
366
383
|
"""Return the url to a page displaying info about the current user."""
|
367
|
-
if not self.security_manager.user_view
|
384
|
+
if not self.security_manager.user_view or self.appbuilder.get_app.config.get(
|
385
|
+
"AUTH_ROLE_PUBLIC", None
|
386
|
+
):
|
368
387
|
return None
|
369
388
|
return url_for(f"{self.security_manager.user_view.endpoint}.userinfo")
|
370
389
|
|
390
|
+
def register_views(self) -> None:
|
391
|
+
self.security_manager.register_views()
|
392
|
+
|
371
393
|
def _is_authorized(
|
372
394
|
self,
|
373
395
|
*,
|
@@ -519,9 +541,11 @@ class FabAuthManager(BaseAuthManager):
|
|
519
541
|
# Otherwise, when the name of a view or menu is changed, the framework
|
520
542
|
# will add the new Views and Menus names to the backend, but will not
|
521
543
|
# delete the old ones.
|
522
|
-
if
|
523
|
-
|
524
|
-
|
544
|
+
if Version(Version(version).base_version) >= Version("3.0.0"):
|
545
|
+
fallback = None
|
546
|
+
else:
|
547
|
+
fallback = conf.getboolean("webserver", "UPDATE_FAB_PERMS")
|
548
|
+
if conf.getboolean("fab", "UPDATE_FAB_PERMS", fallback=fallback):
|
525
549
|
self.security_manager.sync_roles()
|
526
550
|
|
527
551
|
|
@@ -23,6 +23,7 @@ import datetime
|
|
23
23
|
# Copyright 2013, Daniel Vaz Gaspar
|
24
24
|
from typing import TYPE_CHECKING
|
25
25
|
|
26
|
+
import packaging.version
|
26
27
|
from flask import current_app, g
|
27
28
|
from flask_appbuilder.models.sqla import Model
|
28
29
|
from sqlalchemy import (
|
@@ -32,6 +33,7 @@ from sqlalchemy import (
|
|
32
33
|
ForeignKey,
|
33
34
|
Index,
|
34
35
|
Integer,
|
36
|
+
MetaData,
|
35
37
|
String,
|
36
38
|
Table,
|
37
39
|
UniqueConstraint,
|
@@ -39,16 +41,11 @@ from sqlalchemy import (
|
|
39
41
|
func,
|
40
42
|
select,
|
41
43
|
)
|
42
|
-
from sqlalchemy.orm import backref, declared_attr, relationship
|
44
|
+
from sqlalchemy.orm import backref, declared_attr, registry, relationship
|
43
45
|
|
46
|
+
from airflow import __version__ as airflow_version
|
44
47
|
from airflow.auth.managers.models.base_user import BaseUser
|
45
|
-
from airflow.models.base import
|
46
|
-
|
47
|
-
"""
|
48
|
-
Compatibility note: The models in this file are duplicated from Flask AppBuilder.
|
49
|
-
"""
|
50
|
-
# Use airflow metadata to create the tables
|
51
|
-
Model.metadata = Base.metadata
|
48
|
+
from airflow.models.base import _get_schema, naming_convention
|
52
49
|
|
53
50
|
if TYPE_CHECKING:
|
54
51
|
try:
|
@@ -56,6 +53,22 @@ if TYPE_CHECKING:
|
|
56
53
|
except Exception:
|
57
54
|
Identity = None
|
58
55
|
|
56
|
+
"""
|
57
|
+
Compatibility note: The models in this file are duplicated from Flask AppBuilder.
|
58
|
+
"""
|
59
|
+
|
60
|
+
metadata = MetaData(schema=_get_schema(), naming_convention=naming_convention)
|
61
|
+
mapper_registry = registry(metadata=metadata)
|
62
|
+
|
63
|
+
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) >= packaging.version.parse(
|
64
|
+
"3.0.0"
|
65
|
+
):
|
66
|
+
Model.metadata = metadata
|
67
|
+
else:
|
68
|
+
from airflow.models.base import Base
|
69
|
+
|
70
|
+
Model.metadata = Base.metadata
|
71
|
+
|
59
72
|
|
60
73
|
class Action(Model):
|
61
74
|
"""Represents permission actions such as `can_read`."""
|
@@ -29,10 +29,13 @@ class AnonymousUser(AnonymousUserMixin, BaseUser):
|
|
29
29
|
_roles: set[tuple[str, str]] = set()
|
30
30
|
_perms: set[tuple[str, str]] = set()
|
31
31
|
|
32
|
+
first_name = "Anonymous"
|
33
|
+
last_name = ""
|
34
|
+
|
32
35
|
@property
|
33
36
|
def roles(self):
|
34
37
|
if not self._roles:
|
35
|
-
public_role = current_app.appbuilder.get_app.config
|
38
|
+
public_role = current_app.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None)
|
36
39
|
self._roles = {current_app.appbuilder.sm.find_role(public_role)} if public_role else set()
|
37
40
|
return self._roles
|
38
41
|
|
@@ -48,3 +51,6 @@ class AnonymousUser(AnonymousUserMixin, BaseUser):
|
|
48
51
|
(perm.action.name, perm.resource.name) for role in self.roles for perm in role.permissions
|
49
52
|
}
|
50
53
|
return self._perms
|
54
|
+
|
55
|
+
def get_name(self) -> str:
|
56
|
+
return "Anonymous"
|
@@ -0,0 +1,107 @@
|
|
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
|
+
import os
|
20
|
+
|
21
|
+
import airflow
|
22
|
+
from airflow import settings
|
23
|
+
from airflow.exceptions import AirflowException
|
24
|
+
from airflow.providers.fab.auth_manager.models import metadata
|
25
|
+
from airflow.utils.db import _offline_migration, print_happy_cat
|
26
|
+
from airflow.utils.db_manager import BaseDBManager
|
27
|
+
|
28
|
+
PACKAGE_DIR = os.path.dirname(airflow.__file__)
|
29
|
+
|
30
|
+
_REVISION_HEADS_MAP: dict[str, str] = {
|
31
|
+
"1.4.0": "6709f7a774b9",
|
32
|
+
}
|
33
|
+
|
34
|
+
|
35
|
+
class FABDBManager(BaseDBManager):
|
36
|
+
"""Manages FAB database."""
|
37
|
+
|
38
|
+
metadata = metadata
|
39
|
+
version_table_name = "alembic_version_fab"
|
40
|
+
migration_dir = os.path.join(PACKAGE_DIR, "providers/fab/migrations")
|
41
|
+
alembic_file = os.path.join(PACKAGE_DIR, "providers/fab/alembic.ini")
|
42
|
+
supports_table_dropping = True
|
43
|
+
|
44
|
+
def upgradedb(self, to_revision=None, from_revision=None, show_sql_only=False):
|
45
|
+
"""Upgrade the database."""
|
46
|
+
if from_revision and not show_sql_only:
|
47
|
+
raise AirflowException("`from_revision` only supported with `sql_only=True`.")
|
48
|
+
|
49
|
+
# alembic adds significant import time, so we import it lazily
|
50
|
+
if not settings.SQL_ALCHEMY_CONN:
|
51
|
+
raise RuntimeError("The settings.SQL_ALCHEMY_CONN not set. This is a critical assertion.")
|
52
|
+
from alembic import command
|
53
|
+
|
54
|
+
config = self.get_alembic_config()
|
55
|
+
|
56
|
+
if show_sql_only:
|
57
|
+
if settings.engine.dialect.name == "sqlite":
|
58
|
+
raise SystemExit("Offline migration not supported for SQLite.")
|
59
|
+
if not from_revision:
|
60
|
+
from_revision = self.get_current_revision()
|
61
|
+
|
62
|
+
if not to_revision:
|
63
|
+
script = self.get_script_object(config)
|
64
|
+
to_revision = script.get_current_head()
|
65
|
+
|
66
|
+
if to_revision == from_revision:
|
67
|
+
print_happy_cat("No migrations to apply; nothing to do.")
|
68
|
+
return
|
69
|
+
_offline_migration(command.upgrade, config, f"{from_revision}:{to_revision}")
|
70
|
+
return # only running sql; our job is done
|
71
|
+
|
72
|
+
if not self.get_current_revision():
|
73
|
+
# New DB; initialize and exit
|
74
|
+
self.initdb()
|
75
|
+
return
|
76
|
+
|
77
|
+
command.upgrade(config, revision=to_revision or "heads")
|
78
|
+
|
79
|
+
def downgrade(self, to_revision, from_revision=None, show_sql_only=False):
|
80
|
+
if from_revision and not show_sql_only:
|
81
|
+
raise ValueError(
|
82
|
+
"`from_revision` can't be combined with `show_sql_only=False`. When actually "
|
83
|
+
"applying a downgrade (instead of just generating sql), we always "
|
84
|
+
"downgrade from current revision."
|
85
|
+
)
|
86
|
+
|
87
|
+
if not settings.SQL_ALCHEMY_CONN:
|
88
|
+
raise RuntimeError("The settings.SQL_ALCHEMY_CONN not set.")
|
89
|
+
|
90
|
+
# alembic adds significant import time, so we import it lazily
|
91
|
+
from alembic import command
|
92
|
+
|
93
|
+
self.log.info("Attempting downgrade of FAB migration to revision %s", to_revision)
|
94
|
+
config = self.get_alembic_config()
|
95
|
+
|
96
|
+
if show_sql_only:
|
97
|
+
self.log.warning("Generating sql scripts for manual migration.")
|
98
|
+
if not from_revision:
|
99
|
+
from_revision = self.get_current_revision()
|
100
|
+
if from_revision is None:
|
101
|
+
self.log.info("No revision found")
|
102
|
+
return
|
103
|
+
revision_range = f"{from_revision}:{to_revision}"
|
104
|
+
_offline_migration(command.downgrade, config=config, revision=revision_range)
|
105
|
+
else:
|
106
|
+
self.log.info("Applying FAB downgrade migrations.")
|
107
|
+
command.downgrade(config, revision=to_revision, sql=show_sql_only)
|
@@ -609,7 +609,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
609
609
|
@property
|
610
610
|
def auth_role_public(self):
|
611
611
|
"""Get the public role."""
|
612
|
-
return self.appbuilder.get_app.config
|
612
|
+
return self.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None)
|
613
613
|
|
614
614
|
@property
|
615
615
|
def oauth_providers(self):
|
@@ -832,7 +832,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
832
832
|
app = self.appbuilder.get_app
|
833
833
|
# Base Security Config
|
834
834
|
app.config.setdefault("AUTH_ROLE_ADMIN", "Admin")
|
835
|
-
app.config.setdefault("AUTH_ROLE_PUBLIC", "Public")
|
836
835
|
app.config.setdefault("AUTH_TYPE", AUTH_DB)
|
837
836
|
# Self Registration
|
838
837
|
app.config.setdefault("AUTH_USER_REGISTRATION", False)
|
@@ -955,7 +954,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
955
954
|
self.add_role(role_name)
|
956
955
|
if self.auth_role_admin not in self._builtin_roles:
|
957
956
|
self.add_role(self.auth_role_admin)
|
958
|
-
self.
|
957
|
+
if self.auth_role_public:
|
958
|
+
self.add_role(self.auth_role_public)
|
959
959
|
if self.count_users() == 0 and self.auth_role_public != self.auth_role_admin:
|
960
960
|
log.warning(const.LOGMSG_WAR_SEC_NO_USER)
|
961
961
|
except Exception:
|
@@ -1073,6 +1073,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1073
1073
|
dags = dagbag.dags.values()
|
1074
1074
|
|
1075
1075
|
for dag in dags:
|
1076
|
+
# TODO: Remove this when the minimum version of Airflow is bumped to 3.0
|
1076
1077
|
root_dag_id = (getattr(dag, "parent_dag", None) or dag).dag_id
|
1077
1078
|
for resource_name, resource_values in self.RESOURCE_DETAILS_MAP.items():
|
1078
1079
|
dag_resource_name = self._resource_name(root_dag_id, resource_name)
|
@@ -2828,6 +2829,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2828
2829
|
).all()
|
2829
2830
|
|
2830
2831
|
def _get_root_dag_id(self, dag_id: str) -> str:
|
2832
|
+
# TODO: The "root_dag_id" check can be remove when the minimum version of Airflow is bumped to 3.0
|
2831
2833
|
if "." in dag_id and hasattr(DagModel, "root_dag_id"):
|
2832
2834
|
dm = self.appbuilder.get_session.execute(
|
2833
2835
|
select(DagModel.dag_id, DagModel.root_dag_id).where(DagModel.dag_id == dag_id)
|
@@ -28,8 +28,9 @@ def get_provider_info():
|
|
28
28
|
"name": "Fab",
|
29
29
|
"description": "`Flask App Builder <https://flask-appbuilder.readthedocs.io/>`__\n",
|
30
30
|
"state": "ready",
|
31
|
-
"source-date-epoch":
|
31
|
+
"source-date-epoch": 1726860772,
|
32
32
|
"versions": [
|
33
|
+
"1.4.0",
|
33
34
|
"1.3.0",
|
34
35
|
"1.2.2",
|
35
36
|
"1.2.1",
|
@@ -50,6 +51,8 @@ def get_provider_info():
|
|
50
51
|
"google-re2>=1.0",
|
51
52
|
"jmespath>=0.7.0",
|
52
53
|
],
|
54
|
+
"additional-extras": [{"name": "kerberos", "dependencies": ["kerberos>=1.3.0"]}],
|
55
|
+
"devel-dependencies": ["kerberos>=1.3.0"],
|
53
56
|
"config": {
|
54
57
|
"fab": {
|
55
58
|
"description": "This section contains configs specific to FAB provider.",
|
@@ -0,0 +1 @@
|
|
1
|
+
Generic single-database configuration.
|
@@ -0,0 +1,16 @@
|
|
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.
|
@@ -0,0 +1,125 @@
|
|
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
|
+
import contextlib
|
20
|
+
from logging.config import fileConfig
|
21
|
+
|
22
|
+
from alembic import context
|
23
|
+
|
24
|
+
from airflow import settings
|
25
|
+
from airflow.providers.fab.auth_manager.models.db import FABDBManager
|
26
|
+
|
27
|
+
# this is the Alembic Config object, which provides
|
28
|
+
# access to the values within the .ini file in use.
|
29
|
+
config = context.config
|
30
|
+
|
31
|
+
version_table = FABDBManager.version_table_name
|
32
|
+
|
33
|
+
# Interpret the config file for Python logging.
|
34
|
+
# This line sets up loggers basically.
|
35
|
+
if config.config_file_name is not None:
|
36
|
+
fileConfig(config.config_file_name, disable_existing_loggers=False)
|
37
|
+
|
38
|
+
# add your model's MetaData object here
|
39
|
+
# for 'autogenerate' support
|
40
|
+
# from myapp import mymodel
|
41
|
+
# target_metadata = mymodel.Base.metadata
|
42
|
+
target_metadata = FABDBManager.metadata
|
43
|
+
|
44
|
+
# other values from the config, defined by the needs of env.py,
|
45
|
+
# can be acquired:
|
46
|
+
# my_important_option = config.get_main_option("my_important_option")
|
47
|
+
# ... etc.
|
48
|
+
|
49
|
+
|
50
|
+
def include_object(_, name, type_, *args):
|
51
|
+
if type_ == "table" and name not in target_metadata.tables:
|
52
|
+
return False
|
53
|
+
return True
|
54
|
+
|
55
|
+
|
56
|
+
def run_migrations_offline():
|
57
|
+
"""
|
58
|
+
Run migrations in 'offline' mode.
|
59
|
+
|
60
|
+
This configures the context with just a URL
|
61
|
+
and not an Engine, though an Engine is acceptable
|
62
|
+
here as well. By skipping the Engine creation
|
63
|
+
we don't even need a DBAPI to be available.
|
64
|
+
|
65
|
+
Calls to context.execute() here emit the given string to the
|
66
|
+
script output.
|
67
|
+
|
68
|
+
"""
|
69
|
+
context.configure(
|
70
|
+
url=settings.SQL_ALCHEMY_CONN,
|
71
|
+
target_metadata=target_metadata,
|
72
|
+
literal_binds=True,
|
73
|
+
compare_type=True,
|
74
|
+
compare_server_default=True,
|
75
|
+
render_as_batch=True,
|
76
|
+
version_table=version_table,
|
77
|
+
include_object=include_object,
|
78
|
+
)
|
79
|
+
|
80
|
+
with context.begin_transaction():
|
81
|
+
context.run_migrations()
|
82
|
+
|
83
|
+
|
84
|
+
def run_migrations_online():
|
85
|
+
"""
|
86
|
+
Run migrations in 'online' mode.
|
87
|
+
|
88
|
+
In this scenario we need to create an Engine
|
89
|
+
and associate a connection with the context.
|
90
|
+
|
91
|
+
"""
|
92
|
+
|
93
|
+
def process_revision_directives(context, revision, directives):
|
94
|
+
if getattr(config.cmd_opts, "autogenerate", False):
|
95
|
+
script = directives[0]
|
96
|
+
if script.upgrade_ops and script.upgrade_ops.is_empty():
|
97
|
+
directives[:] = []
|
98
|
+
print("No change detected in ORM schema, skipping revision.")
|
99
|
+
|
100
|
+
with contextlib.ExitStack() as stack:
|
101
|
+
connection = config.attributes.get("connection", None)
|
102
|
+
|
103
|
+
if not connection:
|
104
|
+
connection = stack.push(settings.engine.connect())
|
105
|
+
|
106
|
+
context.configure(
|
107
|
+
connection=connection,
|
108
|
+
transaction_per_migration=True,
|
109
|
+
target_metadata=target_metadata,
|
110
|
+
compare_type=True,
|
111
|
+
compare_server_default=True,
|
112
|
+
include_object=include_object,
|
113
|
+
render_as_batch=True,
|
114
|
+
process_revision_directives=process_revision_directives,
|
115
|
+
version_table=version_table,
|
116
|
+
)
|
117
|
+
|
118
|
+
with context.begin_transaction():
|
119
|
+
context.run_migrations()
|
120
|
+
|
121
|
+
|
122
|
+
if context.is_offline_mode():
|
123
|
+
run_migrations_offline()
|
124
|
+
else:
|
125
|
+
run_migrations_online()
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
4
|
+
# distributed with this work for additional information
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
7
|
+
# "License"); you may not use this file except in compliance
|
8
|
+
# with the License. You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
13
|
+
# software distributed under the License is distributed on an
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
15
|
+
# KIND, either express or implied. See the License for the
|
16
|
+
# specific language governing permissions and limitations
|
17
|
+
# under the License.
|
18
|
+
|
19
|
+
"""${message}
|
20
|
+
|
21
|
+
Revision ID: ${up_revision}
|
22
|
+
Revises: ${down_revision | comma,n}
|
23
|
+
Create Date: ${create_date}
|
24
|
+
|
25
|
+
"""
|
26
|
+
from typing import Sequence, Union
|
27
|
+
|
28
|
+
from alembic import op
|
29
|
+
import sqlalchemy as sa
|
30
|
+
${imports if imports else ""}
|
31
|
+
|
32
|
+
# revision identifiers, used by Alembic.
|
33
|
+
revision = ${repr(up_revision)}
|
34
|
+
down_revision = ${repr(down_revision)}
|
35
|
+
branch_labels = ${repr(branch_labels)}
|
36
|
+
depends_on = ${repr(depends_on)}
|
37
|
+
fab_version = None
|
38
|
+
|
39
|
+
|
40
|
+
def upgrade() -> None:
|
41
|
+
${upgrades if upgrades else "pass"}
|
42
|
+
|
43
|
+
|
44
|
+
def downgrade() -> None:
|
45
|
+
${downgrades if downgrades else "pass"}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
4
|
+
# distributed with this work for additional information
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
7
|
+
# "License"); you may not use this file except in compliance
|
8
|
+
# with the License. You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
13
|
+
# software distributed under the License is distributed on an
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
15
|
+
# KIND, either express or implied. See the License for the
|
16
|
+
# specific language governing permissions and limitations
|
17
|
+
# under the License.
|
18
|
+
|
19
|
+
"""
|
20
|
+
placeholder migration.
|
21
|
+
|
22
|
+
Revision ID: 6709f7a774b9
|
23
|
+
Revises:
|
24
|
+
Create Date: 2024-09-03 17:06:38.040510
|
25
|
+
|
26
|
+
Note: This is a placeholder migration used to stamp the migration
|
27
|
+
when we create the migration from the ORM. Otherwise, it will run
|
28
|
+
without stamping the migration, leading to subsequent changes to
|
29
|
+
the tables not being migrated.
|
30
|
+
"""
|
31
|
+
|
32
|
+
from __future__ import annotations
|
33
|
+
|
34
|
+
# revision identifiers, used by Alembic.
|
35
|
+
revision = "6709f7a774b9"
|
36
|
+
down_revision = None
|
37
|
+
branch_labels = None
|
38
|
+
depends_on = None
|
39
|
+
fab_version = "1.4.0"
|
40
|
+
|
41
|
+
|
42
|
+
def upgrade() -> None: ...
|
43
|
+
|
44
|
+
|
45
|
+
def downgrade() -> None: ...
|
@@ -0,0 +1,16 @@
|
|
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.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: apache-airflow-providers-fab
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4.0rc1
|
4
4
|
Summary: Provider package apache-airflow-providers-fab for Apache Airflow
|
5
5
|
Keywords: airflow-provider,fab,airflow,integration
|
6
6
|
Author-email: Apache Software Foundation <dev@airflow.apache.org>
|
@@ -27,13 +27,15 @@ Requires-Dist: flask-login>=0.6.2
|
|
27
27
|
Requires-Dist: flask>=2.2,<2.3
|
28
28
|
Requires-Dist: google-re2>=1.0
|
29
29
|
Requires-Dist: jmespath>=0.7.0
|
30
|
+
Requires-Dist: kerberos>=1.3.0 ; extra == "kerberos"
|
30
31
|
Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
|
31
|
-
Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-fab/1.
|
32
|
-
Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-fab/1.
|
32
|
+
Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-fab/1.4.0/changelog.html
|
33
|
+
Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-fab/1.4.0
|
33
34
|
Project-URL: Slack Chat, https://s.apache.org/airflow-slack
|
34
35
|
Project-URL: Source Code, https://github.com/apache/airflow
|
35
36
|
Project-URL: Twitter, https://twitter.com/ApacheAirflow
|
36
37
|
Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
|
38
|
+
Provides-Extra: kerberos
|
37
39
|
|
38
40
|
|
39
41
|
.. Licensed to the Apache Software Foundation (ASF) under one
|
@@ -79,7 +81,7 @@ Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
|
|
79
81
|
|
80
82
|
Package ``apache-airflow-providers-fab``
|
81
83
|
|
82
|
-
Release: ``1.
|
84
|
+
Release: ``1.4.0.rc1``
|
83
85
|
|
84
86
|
|
85
87
|
`Flask App Builder <https://flask-appbuilder.readthedocs.io/>`__
|
@@ -92,7 +94,7 @@ This is a provider package for ``fab`` provider. All classes for this provider p
|
|
92
94
|
are in ``airflow.providers.fab`` python package.
|
93
95
|
|
94
96
|
You can find package information and changelog for the provider
|
95
|
-
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-fab/1.
|
97
|
+
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-fab/1.4.0/>`_.
|
96
98
|
|
97
99
|
Installation
|
98
100
|
------------
|
@@ -118,4 +120,4 @@ PIP package Version required
|
|
118
120
|
==================== ==================
|
119
121
|
|
120
122
|
The changelog for the provider package can be found in the
|
121
|
-
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-fab/1.
|
123
|
+
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-fab/1.4.0/changelog.html>`_.
|
@@ -1,38 +1,47 @@
|
|
1
1
|
airflow/providers/fab/LICENSE,sha256=FFb4jd2AXnOOf7XLP04pQW6jbdhG49TxlGY6fFpCV1Y,13609
|
2
|
-
airflow/providers/fab/__init__.py,sha256=
|
3
|
-
airflow/providers/fab/
|
2
|
+
airflow/providers/fab/__init__.py,sha256=cDsr9zaomdLZDM0OHErGAqqk62gOk00enaPd7Wggbzs,1490
|
3
|
+
airflow/providers/fab/alembic.ini,sha256=_1SvObfjMAkuD7DN5VR2S6Rd7_F81pORZT-w7GJldIA,4461
|
4
|
+
airflow/providers/fab/get_provider_info.py,sha256=pmrrrCTCFbUghKT42uXbxRWK5ZKnEaYEMRlleaRSHEg,3351
|
4
5
|
airflow/providers/fab/auth_manager/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
|
5
|
-
airflow/providers/fab/auth_manager/fab_auth_manager.py,sha256=
|
6
|
+
airflow/providers/fab/auth_manager/fab_auth_manager.py,sha256=RQzFmULrGVre0GHr6MiVAL2_YqzKTf45LoLHpVjUkMY,21949
|
6
7
|
airflow/providers/fab/auth_manager/api/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
|
7
8
|
airflow/providers/fab/auth_manager/api/auth/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
|
8
9
|
airflow/providers/fab/auth_manager/api/auth/backend/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
|
9
|
-
airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py,sha256=
|
10
|
-
airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py,sha256=
|
10
|
+
airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py,sha256=qj9dIyfNXPoZQUUv9JcxgfNK5swS4aTnPcRa0wcE9Gg,2608
|
11
|
+
airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py,sha256=KAFpis1g8xLDqZsBIzcLGXcGJSpZ_2DTgByHB9uEwqQ,5068
|
11
12
|
airflow/providers/fab/auth_manager/api_endpoints/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
|
12
13
|
airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py,sha256=jr-nna1-QLwfquCR0FJcETlQOMC-L1Mhknf2-bvqITw,7552
|
13
14
|
airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py,sha256=TPT4C02jjOXAWI-lOwnaDeS-EEWzJCjQuFVc2E4JsPw,8484
|
14
15
|
airflow/providers/fab/auth_manager/cli_commands/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
|
15
|
-
airflow/providers/fab/auth_manager/cli_commands/
|
16
|
+
airflow/providers/fab/auth_manager/cli_commands/db_command.py,sha256=h0yRnHMKs1Dndujt5FKyWOv3yEZnX0L41rUadbyuP98,2189
|
17
|
+
airflow/providers/fab/auth_manager/cli_commands/definition.py,sha256=XIAPqoHwkFHshfbj6yGIs7xUrR-uSE1F7V71n44PttA,11483
|
16
18
|
airflow/providers/fab/auth_manager/cli_commands/role_command.py,sha256=4w1tHTR5gBbsymeqGIJ4Rs8CmGdw0l49y58pfI0DB_s,9098
|
17
19
|
airflow/providers/fab/auth_manager/cli_commands/sync_perm_command.py,sha256=VpW-rWhgHAL_ReU66D_BrsxlXQN4okfxzj6dyE5IfwA,1663
|
18
|
-
airflow/providers/fab/auth_manager/cli_commands/user_command.py,sha256=
|
20
|
+
airflow/providers/fab/auth_manager/cli_commands/user_command.py,sha256=DMsGccMSs2u0cn1dQgRYkJoG37JhEE-m9dtn-sWEOXw,10275
|
19
21
|
airflow/providers/fab/auth_manager/cli_commands/utils.py,sha256=yr4CMaP5Y6-0mRwsPl62aPEoiys8ImyOlO0e9CLyrGY,2043
|
20
22
|
airflow/providers/fab/auth_manager/decorators/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
|
21
23
|
airflow/providers/fab/auth_manager/decorators/auth.py,sha256=Z4_xiS1N1E0MqDP9OxHx_YJvKe0sH15afP0tn5LP3Ss,4948
|
22
|
-
airflow/providers/fab/auth_manager/models/__init__.py,sha256=
|
23
|
-
airflow/providers/fab/auth_manager/models/anonymous_user.py,sha256=
|
24
|
+
airflow/providers/fab/auth_manager/models/__init__.py,sha256=KZl07tphFlZ8kTnsV8k7eTMilaWtd6rV-RjtWOkb13E,9243
|
25
|
+
airflow/providers/fab/auth_manager/models/anonymous_user.py,sha256=CNwZHKyURxkTSkqhnyGioML9qBsngDP0wmfdFe04w8E,1893
|
26
|
+
airflow/providers/fab/auth_manager/models/db.py,sha256=lidEkcdFZ7jR-Sva0z3JLhfba0X3k4cOPmQM5wyzke4,4375
|
24
27
|
airflow/providers/fab/auth_manager/openapi/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
|
25
28
|
airflow/providers/fab/auth_manager/openapi/v1.yaml,sha256=xFlQMccLoGarx8SnQvGJ1DRfHo4jQ3p9DzoPqQINcCw,19380
|
26
29
|
airflow/providers/fab/auth_manager/security_manager/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
|
27
30
|
airflow/providers/fab/auth_manager/security_manager/constants.py,sha256=x1Sjl_Mu3wmaSy3NFZlHxK2z-juzWmMs1SrzJ0aiBBQ,907
|
28
|
-
airflow/providers/fab/auth_manager/security_manager/override.py,sha256=
|
31
|
+
airflow/providers/fab/auth_manager/security_manager/override.py,sha256=uzHY9zftcSdJXozsgKdfR9KK7HIK_RYQa7Ad1nP7IMY,115425
|
29
32
|
airflow/providers/fab/auth_manager/views/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
|
30
33
|
airflow/providers/fab/auth_manager/views/permissions.py,sha256=k5APUTqqKZqMxCWlKrUEgqdDVrGa9719HJ3UmFpiSLc,2886
|
31
34
|
airflow/providers/fab/auth_manager/views/roles_list.py,sha256=o_VqnLbQa4sisKxLwyx6VCl3HmvjKcjkrJG23R81FMQ,1494
|
32
35
|
airflow/providers/fab/auth_manager/views/user.py,sha256=KaBWeBMy5j8o9qwe0RDHhtToO4aC6pH_m7N9IcIj1xM,5995
|
33
36
|
airflow/providers/fab/auth_manager/views/user_edit.py,sha256=d8wQ_8Rk8Ce95sCElZIXN7ATwbXrT2Tauqn5uTwuo2E,2140
|
34
37
|
airflow/providers/fab/auth_manager/views/user_stats.py,sha256=zP2eX6e40rpEohJcvnvri4Khu3Q4ULLxjz1zOWvOr8A,1300
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
airflow/providers/fab/migrations/README,sha256=heMzebYwlGhnE8_4CWJ4LS74WoEZjBy-S-mIJRxAEKI,39
|
39
|
+
airflow/providers/fab/migrations/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
|
40
|
+
airflow/providers/fab/migrations/env.py,sha256=brxmsuAiSRSwzG14Butue1chSJSXaLW53-ABihPLmKw,3951
|
41
|
+
airflow/providers/fab/migrations/script.py.mako,sha256=_krfPtzv1Unme2b9MCHPgXo0g73HcN2_zDgz3co2N4M,1353
|
42
|
+
airflow/providers/fab/migrations/versions/0001_1_4_0_placeholder_migration.py,sha256=wLZiBiWjfSofXZ4jXMjpEKkIc3jI0a6hpJ73xEYAzss,1370
|
43
|
+
airflow/providers/fab/migrations/versions/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
|
44
|
+
apache_airflow_providers_fab-1.4.0rc1.dist-info/entry_points.txt,sha256=m05kASp7vFi0ZmQ--CFp7GeJpPL7UT2RQF8EEP5XRX8,99
|
45
|
+
apache_airflow_providers_fab-1.4.0rc1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
46
|
+
apache_airflow_providers_fab-1.4.0rc1.dist-info/METADATA,sha256=LZQ-8b5LI6Fb-kqfgQVLYrsj4vVYa3X5-MUzKy9ZMgc,5100
|
47
|
+
apache_airflow_providers_fab-1.4.0rc1.dist-info/RECORD,,
|
File without changes
|
File without changes
|