apache-airflow-providers-fab 1.5.3rc1__py3-none-any.whl → 2.0.0b1__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/LICENSE +0 -52
- airflow/providers/fab/__init__.py +3 -3
- airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +3 -3
- airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +4 -4
- airflow/providers/fab/auth_manager/api/auth/backend/session.py +1 -1
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +14 -13
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +12 -12
- airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +152 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
- airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -4
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
- airflow/providers/fab/auth_manager/cli_commands/utils.py +10 -9
- airflow/providers/fab/auth_manager/fab_auth_manager.py +231 -126
- airflow/providers/fab/auth_manager/models/__init__.py +1 -1
- airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
- airflow/providers/fab/auth_manager/models/db.py +22 -5
- airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +71 -632
- airflow/providers/fab/auth_manager/views/permissions.py +1 -1
- airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
- airflow/providers/fab/auth_manager/views/user.py +1 -1
- airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
- airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
- airflow/providers/fab/get_provider_info.py +22 -16
- airflow/providers/fab/www/airflow_flask_app.py +31 -0
- airflow/providers/fab/www/api_connexion/__init__.py +17 -0
- airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
- airflow/providers/fab/www/api_connexion/parameters.py +131 -0
- airflow/providers/fab/www/api_connexion/security.py +84 -0
- airflow/providers/fab/www/api_connexion/types.py +30 -0
- airflow/providers/fab/www/app.py +112 -0
- airflow/providers/fab/www/auth.py +350 -0
- airflow/providers/fab/www/constants.py +28 -0
- airflow/providers/fab/www/extensions/__init__.py +16 -0
- airflow/providers/fab/www/extensions/init_appbuilder.py +602 -0
- airflow/providers/fab/www/extensions/init_jinja_globals.py +82 -0
- airflow/providers/fab/www/extensions/init_manifest_files.py +61 -0
- airflow/providers/fab/www/extensions/init_security.py +61 -0
- airflow/providers/fab/www/extensions/init_session.py +64 -0
- airflow/providers/fab/www/extensions/init_views.py +177 -0
- airflow/providers/fab/www/package-lock.json +10127 -0
- airflow/providers/fab/www/package.json +81 -0
- airflow/providers/fab/www/security/__init__.py +17 -0
- airflow/providers/fab/www/security/permissions.py +126 -0
- airflow/providers/fab/www/security_appless.py +44 -0
- airflow/providers/fab/www/security_manager.py +122 -0
- airflow/providers/fab/www/session.py +41 -0
- airflow/providers/fab/www/static/css/bootstrap-theme.css +6215 -0
- airflow/providers/fab/www/static/css/flash.css +57 -0
- airflow/providers/fab/www/static/css/loading-dots.css +60 -0
- airflow/providers/fab/www/static/css/main.css +676 -0
- airflow/providers/fab/www/static/css/material-icons.css +84 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
- airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.css +18 -0
- airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.js +2 -0
- airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.js.LICENSE.txt +18 -0
- airflow/providers/fab/www/static/dist/manifest.json +17 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
- airflow/providers/fab/www/static/dist/moment.4d28b37c229bdfc54575.js +2 -0
- airflow/providers/fab/www/static/dist/moment.4d28b37c229bdfc54575.js.LICENSE.txt +11 -0
- airflow/providers/fab/www/static/dist/oss-licenses.json +29 -0
- airflow/providers/fab/www/static/js/datetime_utils.js +134 -0
- airflow/providers/fab/www/static/js/main.js +324 -0
- airflow/providers/fab/www/static/sort_asc.png +0 -0
- airflow/providers/fab/www/static/sort_both.png +0 -0
- airflow/providers/fab/www/static/sort_desc.png +0 -0
- airflow/providers/fab/www/templates/airflow/_messages.html +30 -0
- airflow/providers/fab/www/templates/airflow/error.html +35 -0
- airflow/providers/fab/www/templates/airflow/main.html +78 -0
- airflow/providers/fab/www/templates/airflow/traceback.html +53 -0
- airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
- airflow/providers/fab/www/templates/appbuilder/index.html +20 -0
- airflow/providers/fab/www/templates/appbuilder/navbar.html +60 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_menu.html +60 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
- airflow/providers/fab/www/utils.py +272 -0
- airflow/providers/fab/www/views.py +129 -0
- airflow/providers/fab/www/webpack.config.js +213 -0
- {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/METADATA +18 -36
- apache_airflow_providers_fab-2.0.0b1.dist-info/RECORD +122 -0
- {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/WHEEL +1 -1
- airflow/providers/fab/auth_manager/decorators/auth.py +0 -126
- apache_airflow_providers_fab-1.5.3rc1.dist-info/RECORD +0 -51
- /airflow/providers/fab/{auth_manager/decorators → www}/__init__.py +0 -0
- {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,112 @@
|
|
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
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
from os.path import isabs
|
21
|
+
|
22
|
+
from flask import Flask
|
23
|
+
from flask_appbuilder import SQLA
|
24
|
+
from flask_wtf.csrf import CSRFProtect
|
25
|
+
from sqlalchemy.engine.url import make_url
|
26
|
+
|
27
|
+
from airflow import settings
|
28
|
+
from airflow.api_fastapi.app import get_auth_manager
|
29
|
+
from airflow.configuration import conf
|
30
|
+
from airflow.exceptions import AirflowConfigException
|
31
|
+
from airflow.logging_config import configure_logging
|
32
|
+
from airflow.providers.fab.www.extensions.init_appbuilder import init_appbuilder
|
33
|
+
from airflow.providers.fab.www.extensions.init_jinja_globals import init_jinja_globals
|
34
|
+
from airflow.providers.fab.www.extensions.init_manifest_files import configure_manifest_files
|
35
|
+
from airflow.providers.fab.www.extensions.init_security import init_api_auth, init_xframe_protection
|
36
|
+
from airflow.providers.fab.www.extensions.init_session import init_airflow_session_interface
|
37
|
+
from airflow.providers.fab.www.extensions.init_views import (
|
38
|
+
init_api_auth_provider,
|
39
|
+
init_api_error_handlers,
|
40
|
+
init_error_handlers,
|
41
|
+
init_plugins,
|
42
|
+
)
|
43
|
+
|
44
|
+
app: Flask | None = None
|
45
|
+
|
46
|
+
# Initializes at the module level, so plugins can access it.
|
47
|
+
# See: /docs/plugins.rst
|
48
|
+
csrf = CSRFProtect()
|
49
|
+
|
50
|
+
|
51
|
+
def create_app(enable_plugins: bool):
|
52
|
+
"""Create a new instance of Airflow WWW app."""
|
53
|
+
from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager
|
54
|
+
|
55
|
+
flask_app = Flask(__name__)
|
56
|
+
flask_app.secret_key = conf.get("webserver", "SECRET_KEY")
|
57
|
+
flask_app.config["SQLALCHEMY_DATABASE_URI"] = conf.get("database", "SQL_ALCHEMY_CONN")
|
58
|
+
flask_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
59
|
+
|
60
|
+
url = make_url(flask_app.config["SQLALCHEMY_DATABASE_URI"])
|
61
|
+
if url.drivername == "sqlite" and url.database and not isabs(url.database):
|
62
|
+
raise AirflowConfigException(
|
63
|
+
f'Cannot use relative path: `{conf.get("database", "SQL_ALCHEMY_CONN")}` to connect to sqlite. '
|
64
|
+
"Please use absolute path such as `sqlite:////tmp/airflow.db`."
|
65
|
+
)
|
66
|
+
|
67
|
+
if "SQLALCHEMY_ENGINE_OPTIONS" not in flask_app.config:
|
68
|
+
flask_app.config["SQLALCHEMY_ENGINE_OPTIONS"] = settings.prepare_engine_args()
|
69
|
+
|
70
|
+
csrf.init_app(flask_app)
|
71
|
+
|
72
|
+
db = SQLA()
|
73
|
+
db.session = settings.Session
|
74
|
+
db.init_app(flask_app)
|
75
|
+
|
76
|
+
configure_logging()
|
77
|
+
configure_manifest_files(flask_app)
|
78
|
+
init_api_auth(flask_app)
|
79
|
+
|
80
|
+
with flask_app.app_context():
|
81
|
+
init_appbuilder(flask_app, enable_plugins=enable_plugins)
|
82
|
+
init_error_handlers(flask_app)
|
83
|
+
# In two scenarios a Flask application can be created:
|
84
|
+
# - To support Airflow 2 plugins relying on Flask (``enable_plugins`` is True)
|
85
|
+
# - To support FAB auth manager (``enable_plugins`` is False)
|
86
|
+
# There are some edge cases where ``enable_plugins`` is False but the auth manager configured is not
|
87
|
+
# FAB auth manager. One example is ``run_update_fastapi_api_spec``, it calls
|
88
|
+
# ``FabAuthManager().get_fastapi_app()`` to generate the openapi documentation regardless of the
|
89
|
+
# configured auth manager.
|
90
|
+
if enable_plugins:
|
91
|
+
init_plugins(flask_app)
|
92
|
+
elif isinstance(get_auth_manager(), FabAuthManager):
|
93
|
+
init_api_auth_provider(flask_app)
|
94
|
+
init_api_error_handlers(flask_app)
|
95
|
+
init_jinja_globals(flask_app, enable_plugins=enable_plugins)
|
96
|
+
init_xframe_protection(flask_app)
|
97
|
+
init_airflow_session_interface(flask_app)
|
98
|
+
return flask_app
|
99
|
+
|
100
|
+
|
101
|
+
def cached_app():
|
102
|
+
"""Return cached instance of Airflow WWW app."""
|
103
|
+
global app
|
104
|
+
if not app:
|
105
|
+
app = create_app()
|
106
|
+
return app
|
107
|
+
|
108
|
+
|
109
|
+
def purge_cached_app():
|
110
|
+
"""Remove the cached version of the app in global state."""
|
111
|
+
global app
|
112
|
+
app = None
|
@@ -0,0 +1,350 @@
|
|
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 functools
|
20
|
+
import logging
|
21
|
+
from collections.abc import Sequence
|
22
|
+
from functools import wraps
|
23
|
+
from typing import TYPE_CHECKING, Callable, TypeVar, cast
|
24
|
+
|
25
|
+
from flask import flash, redirect, render_template, request, url_for
|
26
|
+
from flask_appbuilder._compat import as_unicode
|
27
|
+
from flask_appbuilder.const import (
|
28
|
+
FLAMSG_ERR_SEC_ACCESS_DENIED,
|
29
|
+
LOGMSG_ERR_SEC_ACCESS_DENIED,
|
30
|
+
PERMISSION_PREFIX,
|
31
|
+
)
|
32
|
+
|
33
|
+
from airflow.api_fastapi.app import get_auth_manager
|
34
|
+
from airflow.api_fastapi.auth.managers.models.resource_details import (
|
35
|
+
AccessView,
|
36
|
+
ConnectionDetails,
|
37
|
+
DagAccessEntity,
|
38
|
+
DagDetails,
|
39
|
+
PoolDetails,
|
40
|
+
VariableDetails,
|
41
|
+
)
|
42
|
+
from airflow.configuration import conf
|
43
|
+
from airflow.providers.fab.www.utils import get_fab_auth_manager
|
44
|
+
from airflow.utils.net import get_hostname
|
45
|
+
|
46
|
+
if TYPE_CHECKING:
|
47
|
+
from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod
|
48
|
+
from airflow.api_fastapi.auth.managers.models.batch_apis import (
|
49
|
+
IsAuthorizedConnectionRequest,
|
50
|
+
IsAuthorizedDagRequest,
|
51
|
+
IsAuthorizedPoolRequest,
|
52
|
+
IsAuthorizedVariableRequest,
|
53
|
+
)
|
54
|
+
from airflow.models import DagRun, Pool, TaskInstance, Variable
|
55
|
+
from airflow.models.connection import Connection
|
56
|
+
from airflow.models.xcom import XComModel
|
57
|
+
|
58
|
+
T = TypeVar("T", bound=Callable)
|
59
|
+
|
60
|
+
log = logging.getLogger(__name__)
|
61
|
+
|
62
|
+
|
63
|
+
def get_access_denied_message():
|
64
|
+
return conf.get("webserver", "access_denied_message")
|
65
|
+
|
66
|
+
|
67
|
+
def has_access_with_pk(f):
|
68
|
+
"""
|
69
|
+
Check permissions on views.
|
70
|
+
|
71
|
+
The implementation is very similar from
|
72
|
+
https://github.com/dpgaspar/Flask-AppBuilder/blob/c6fecdc551629e15467fde5d06b4437379d90592/flask_appbuilder/security/decorators.py#L134
|
73
|
+
|
74
|
+
The difference is that this decorator will pass the resource ID to check permissions. It allows
|
75
|
+
fined-grained access control using resource IDs.
|
76
|
+
"""
|
77
|
+
if hasattr(f, "_permission_name"):
|
78
|
+
permission_str = f._permission_name
|
79
|
+
else:
|
80
|
+
permission_str = f.__name__
|
81
|
+
|
82
|
+
def wraps(self, *args, **kwargs):
|
83
|
+
permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
|
84
|
+
if self.method_permission_name:
|
85
|
+
_permission_name = self.method_permission_name.get(f.__name__)
|
86
|
+
if _permission_name:
|
87
|
+
permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
|
88
|
+
if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
|
89
|
+
action_name=permission_str,
|
90
|
+
resource_name=self.class_permission_name,
|
91
|
+
resource_pk=kwargs.get("pk"),
|
92
|
+
):
|
93
|
+
return f(self, *args, **kwargs)
|
94
|
+
else:
|
95
|
+
log.warning(LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, self.__class__.__name__)
|
96
|
+
flash(as_unicode(FLAMSG_ERR_SEC_ACCESS_DENIED), "danger")
|
97
|
+
return redirect(get_auth_manager().get_url_login(next_url=request.url))
|
98
|
+
|
99
|
+
f._permission_name = permission_str
|
100
|
+
return functools.update_wrapper(wraps, f)
|
101
|
+
|
102
|
+
|
103
|
+
def _has_access_no_details(is_authorized_callback: Callable[[], bool]) -> Callable[[T], T]:
|
104
|
+
"""
|
105
|
+
Check current user's permissions against required permissions.
|
106
|
+
|
107
|
+
This works only for resources with no details. This function is used in some ``has_access_`` functions
|
108
|
+
below.
|
109
|
+
|
110
|
+
:param is_authorized_callback: callback to execute to figure whether the user is authorized to access
|
111
|
+
the resource?
|
112
|
+
"""
|
113
|
+
|
114
|
+
def has_access_decorator(func: T):
|
115
|
+
@wraps(func)
|
116
|
+
def decorated(*args, **kwargs):
|
117
|
+
return _has_access(
|
118
|
+
is_authorized=is_authorized_callback(),
|
119
|
+
func=func,
|
120
|
+
args=args,
|
121
|
+
kwargs=kwargs,
|
122
|
+
)
|
123
|
+
|
124
|
+
return cast(T, decorated)
|
125
|
+
|
126
|
+
return has_access_decorator
|
127
|
+
|
128
|
+
|
129
|
+
def _has_access(*, is_authorized: bool, func: Callable, args, kwargs):
|
130
|
+
"""
|
131
|
+
Define the behavior whether the user is authorized to access the resource.
|
132
|
+
|
133
|
+
:param is_authorized: whether the user is authorized to access the resource
|
134
|
+
:param func: the function to call if the user is authorized
|
135
|
+
:param args: the arguments of ``func``
|
136
|
+
:param kwargs: the keyword arguments ``func``
|
137
|
+
|
138
|
+
:meta private:
|
139
|
+
"""
|
140
|
+
if is_authorized:
|
141
|
+
return func(*args, **kwargs)
|
142
|
+
elif get_fab_auth_manager().is_logged_in() and not get_auth_manager().is_authorized_view(
|
143
|
+
access_view=AccessView.WEBSITE,
|
144
|
+
user=get_fab_auth_manager().get_user(),
|
145
|
+
):
|
146
|
+
return (
|
147
|
+
render_template(
|
148
|
+
"airflow/no_roles_permissions.html",
|
149
|
+
hostname=get_hostname() if conf.getboolean("webserver", "EXPOSE_HOSTNAME") else "",
|
150
|
+
logout_url=get_fab_auth_manager().get_url_logout(),
|
151
|
+
),
|
152
|
+
403,
|
153
|
+
)
|
154
|
+
elif not get_fab_auth_manager().is_logged_in():
|
155
|
+
return redirect(get_auth_manager().get_url_login(next_url=request.url))
|
156
|
+
else:
|
157
|
+
access_denied = get_access_denied_message()
|
158
|
+
flash(access_denied, "danger")
|
159
|
+
return redirect(url_for("FabIndexView.index"))
|
160
|
+
|
161
|
+
|
162
|
+
def has_access_configuration(method: ResourceMethod) -> Callable[[T], T]:
|
163
|
+
return _has_access_no_details(
|
164
|
+
lambda: get_auth_manager().is_authorized_configuration(
|
165
|
+
method=method, user=get_fab_auth_manager().get_user()
|
166
|
+
)
|
167
|
+
)
|
168
|
+
|
169
|
+
|
170
|
+
def has_access_connection(method: ResourceMethod) -> Callable[[T], T]:
|
171
|
+
def has_access_decorator(func: T):
|
172
|
+
@wraps(func)
|
173
|
+
def decorated(*args, **kwargs):
|
174
|
+
connections: set[Connection] = set(args[1])
|
175
|
+
requests: Sequence[IsAuthorizedConnectionRequest] = [
|
176
|
+
{
|
177
|
+
"method": method,
|
178
|
+
"details": ConnectionDetails(conn_id=connection.conn_id),
|
179
|
+
}
|
180
|
+
for connection in connections
|
181
|
+
]
|
182
|
+
is_authorized = get_auth_manager().batch_is_authorized_connection(
|
183
|
+
requests, user=get_auth_manager().get_user()
|
184
|
+
)
|
185
|
+
return _has_access(
|
186
|
+
is_authorized=is_authorized,
|
187
|
+
func=func,
|
188
|
+
args=args,
|
189
|
+
kwargs=kwargs,
|
190
|
+
)
|
191
|
+
|
192
|
+
return cast(T, decorated)
|
193
|
+
|
194
|
+
return has_access_decorator
|
195
|
+
|
196
|
+
|
197
|
+
def has_access_dag(method: ResourceMethod, access_entity: DagAccessEntity | None = None) -> Callable[[T], T]:
|
198
|
+
def has_access_decorator(func: T):
|
199
|
+
@wraps(func)
|
200
|
+
def decorated(*args, **kwargs):
|
201
|
+
dag_id_kwargs = kwargs.get("dag_id")
|
202
|
+
dag_id_args = request.args.get("dag_id")
|
203
|
+
dag_id_form = request.form.get("dag_id")
|
204
|
+
dag_id_json = request.json.get("dag_id") if request.is_json else None
|
205
|
+
all_dag_ids = [dag_id_kwargs, dag_id_args, dag_id_form, dag_id_json]
|
206
|
+
unique_dag_ids = set(dag_id for dag_id in all_dag_ids if dag_id is not None)
|
207
|
+
|
208
|
+
if len(unique_dag_ids) > 1:
|
209
|
+
log.warning(
|
210
|
+
"There are different dag_ids passed in the request: %s. Returning 403.", unique_dag_ids
|
211
|
+
)
|
212
|
+
log.warning(
|
213
|
+
"kwargs: %s, args: %s, form: %s, json: %s",
|
214
|
+
dag_id_kwargs,
|
215
|
+
dag_id_args,
|
216
|
+
dag_id_form,
|
217
|
+
dag_id_json,
|
218
|
+
)
|
219
|
+
return (
|
220
|
+
render_template(
|
221
|
+
"airflow/no_roles_permissions.html",
|
222
|
+
hostname=get_hostname() if conf.getboolean("webserver", "EXPOSE_HOSTNAME") else "",
|
223
|
+
logout_url=get_auth_manager().get_url_logout(),
|
224
|
+
),
|
225
|
+
403,
|
226
|
+
)
|
227
|
+
dag_id = unique_dag_ids.pop() if unique_dag_ids else None
|
228
|
+
|
229
|
+
is_authorized = get_auth_manager().is_authorized_dag(
|
230
|
+
method=method,
|
231
|
+
access_entity=access_entity,
|
232
|
+
details=None if not dag_id else DagDetails(id=dag_id),
|
233
|
+
user=get_auth_manager().get_user(),
|
234
|
+
)
|
235
|
+
|
236
|
+
return _has_access(
|
237
|
+
is_authorized=is_authorized,
|
238
|
+
func=func,
|
239
|
+
args=args,
|
240
|
+
kwargs=kwargs,
|
241
|
+
)
|
242
|
+
|
243
|
+
return cast(T, decorated)
|
244
|
+
|
245
|
+
return has_access_decorator
|
246
|
+
|
247
|
+
|
248
|
+
def has_access_dag_entities(method: ResourceMethod, access_entity: DagAccessEntity) -> Callable[[T], T]:
|
249
|
+
def has_access_decorator(func: T):
|
250
|
+
@wraps(func)
|
251
|
+
def decorated(*args, **kwargs):
|
252
|
+
items: set[XComModel | DagRun | TaskInstance] = set(args[1])
|
253
|
+
requests: Sequence[IsAuthorizedDagRequest] = [
|
254
|
+
{
|
255
|
+
"method": method,
|
256
|
+
"access_entity": access_entity,
|
257
|
+
"details": DagDetails(id=item.dag_id),
|
258
|
+
}
|
259
|
+
for item in items
|
260
|
+
if item is not None
|
261
|
+
]
|
262
|
+
is_authorized = get_auth_manager().batch_is_authorized_dag(
|
263
|
+
requests, user=get_auth_manager().get_user()
|
264
|
+
)
|
265
|
+
return _has_access(
|
266
|
+
is_authorized=is_authorized,
|
267
|
+
func=func,
|
268
|
+
args=args,
|
269
|
+
kwargs=kwargs,
|
270
|
+
)
|
271
|
+
|
272
|
+
return cast(T, decorated)
|
273
|
+
|
274
|
+
return has_access_decorator
|
275
|
+
|
276
|
+
|
277
|
+
def has_access_asset(method: ResourceMethod) -> Callable[[T], T]:
|
278
|
+
"""Check current user's permissions against required permissions for assets."""
|
279
|
+
return _has_access_no_details(
|
280
|
+
lambda: get_auth_manager().is_authorized_asset(method=method, user=get_fab_auth_manager().get_user())
|
281
|
+
)
|
282
|
+
|
283
|
+
|
284
|
+
def has_access_pool(method: ResourceMethod) -> Callable[[T], T]:
|
285
|
+
def has_access_decorator(func: T):
|
286
|
+
@wraps(func)
|
287
|
+
def decorated(*args, **kwargs):
|
288
|
+
pools: set[Pool] = set(args[1])
|
289
|
+
requests: Sequence[IsAuthorizedPoolRequest] = [
|
290
|
+
{
|
291
|
+
"method": method,
|
292
|
+
"details": PoolDetails(name=pool.pool),
|
293
|
+
}
|
294
|
+
for pool in pools
|
295
|
+
]
|
296
|
+
is_authorized = get_auth_manager().batch_is_authorized_pool(
|
297
|
+
requests, user=get_auth_manager().get_user()
|
298
|
+
)
|
299
|
+
return _has_access(
|
300
|
+
is_authorized=is_authorized,
|
301
|
+
func=func,
|
302
|
+
args=args,
|
303
|
+
kwargs=kwargs,
|
304
|
+
)
|
305
|
+
|
306
|
+
return cast(T, decorated)
|
307
|
+
|
308
|
+
return has_access_decorator
|
309
|
+
|
310
|
+
|
311
|
+
def has_access_variable(method: ResourceMethod) -> Callable[[T], T]:
|
312
|
+
def has_access_decorator(func: T):
|
313
|
+
@wraps(func)
|
314
|
+
def decorated(*args, **kwargs):
|
315
|
+
if len(args) == 1:
|
316
|
+
# No items provided
|
317
|
+
is_authorized = get_auth_manager().is_authorized_variable(
|
318
|
+
method=method, user=get_auth_manager().get_user()
|
319
|
+
)
|
320
|
+
else:
|
321
|
+
variables: set[Variable] = set(args[1])
|
322
|
+
requests: Sequence[IsAuthorizedVariableRequest] = [
|
323
|
+
{
|
324
|
+
"method": method,
|
325
|
+
"details": VariableDetails(key=variable.key),
|
326
|
+
}
|
327
|
+
for variable in variables
|
328
|
+
]
|
329
|
+
is_authorized = get_auth_manager().batch_is_authorized_variable(
|
330
|
+
requests, user=get_auth_manager().get_user()
|
331
|
+
)
|
332
|
+
return _has_access(
|
333
|
+
is_authorized=is_authorized,
|
334
|
+
func=func,
|
335
|
+
args=args,
|
336
|
+
kwargs=kwargs,
|
337
|
+
)
|
338
|
+
|
339
|
+
return cast(T, decorated)
|
340
|
+
|
341
|
+
return has_access_decorator
|
342
|
+
|
343
|
+
|
344
|
+
def has_access_view(access_view: AccessView = AccessView.WEBSITE) -> Callable[[T], T]:
|
345
|
+
"""Check current user's permissions to access the website."""
|
346
|
+
return _has_access_no_details(
|
347
|
+
lambda: get_auth_manager().is_authorized_view(
|
348
|
+
access_view=access_view, user=get_fab_auth_manager().get_user()
|
349
|
+
)
|
350
|
+
)
|
@@ -0,0 +1,28 @@
|
|
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 pathlib import Path
|
20
|
+
|
21
|
+
from airflow.configuration import conf
|
22
|
+
|
23
|
+
WWW = Path(__file__).resolve().parent
|
24
|
+
# There is a difference with configuring Swagger in Connexion 2.x and Connexion 3.x
|
25
|
+
# Connexion 2: https://connexion.readthedocs.io/en/2.14.2/quickstart.html#the-swagger-ui-console
|
26
|
+
# Connexion 3: https://connexion.readthedocs.io/en/stable/swagger_ui.html#configuring-the-swagger-ui
|
27
|
+
SWAGGER_ENABLED = conf.getboolean("webserver", "enable_swagger_ui", fallback=True)
|
28
|
+
SWAGGER_BUNDLE = WWW.joinpath("static", "dist", "swagger-ui")
|
@@ -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.
|