apache-airflow-providers-fab 1.5.3rc1__py3-none-any.whl → 2.0.0__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 +4 -5
- airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +5 -5
- airflow/providers/fab/auth_manager/api/auth/backend/session.py +2 -2
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +15 -15
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +13 -14
- 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 +153 -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 +1 -3
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
- airflow/providers/fab/auth_manager/cli_commands/utils.py +12 -11
- airflow/providers/fab/auth_manager/fab_auth_manager.py +238 -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/openapi/v1.yaml +9 -0
- airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +186 -655
- 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 +29 -34
- 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 +120 -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 +606 -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 +8939 -0
- airflow/providers/fab/www/package.json +77 -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/48f0ea180c40270a5b05.png +1 -0
- airflow/providers/fab/www/static/dist/649c0b07771e68fafdeb.png +1 -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/f7490d556a6c42e49ba4.png +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.edb2d40dfbbc537916e3.css +18 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js +2 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js.LICENSE.txt +18 -0
- airflow/providers/fab/www/static/dist/manifest.json +20 -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.624b1f00ba723d39ce06.js +2 -0
- airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js.LICENSE.txt +11 -0
- airflow/providers/fab/www/static/dist/oss-licenses.json +20 -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 +288 -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.0.dist-info}/METADATA +30 -38
- apache_airflow_providers_fab-2.0.0.dist-info/RECORD +125 -0
- {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0.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.0.dist-info}/entry_points.txt +0 -0
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|
18
18
|
|
19
19
|
from flask_appbuilder.security.views import UserStatsChartView
|
20
20
|
|
21
|
-
from airflow.security import permissions
|
21
|
+
from airflow.providers.fab.www.security import permissions
|
22
22
|
|
23
23
|
|
24
24
|
class CustomUserStatsChartView(UserStatsChartView):
|
@@ -15,8 +15,7 @@
|
|
15
15
|
# specific language governing permissions and limitations
|
16
16
|
# under the License.
|
17
17
|
|
18
|
-
# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE
|
19
|
-
# OVERWRITTEN WHEN PREPARING PACKAGES.
|
18
|
+
# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN!
|
20
19
|
#
|
21
20
|
# IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE
|
22
21
|
# `get_provider_info_TEMPLATE.py.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY
|
@@ -27,38 +26,6 @@ def get_provider_info():
|
|
27
26
|
"package-name": "apache-airflow-providers-fab",
|
28
27
|
"name": "Fab",
|
29
28
|
"description": "`Flask App Builder <https://flask-appbuilder.readthedocs.io/>`__\n",
|
30
|
-
"state": "ready",
|
31
|
-
"source-date-epoch": 1738677661,
|
32
|
-
"versions": [
|
33
|
-
"1.5.3",
|
34
|
-
"1.5.2",
|
35
|
-
"1.5.1",
|
36
|
-
"1.5.0",
|
37
|
-
"1.4.1",
|
38
|
-
"1.4.0",
|
39
|
-
"1.3.0",
|
40
|
-
"1.2.2",
|
41
|
-
"1.2.1",
|
42
|
-
"1.2.0",
|
43
|
-
"1.1.1",
|
44
|
-
"1.1.0",
|
45
|
-
"1.0.4",
|
46
|
-
"1.0.3",
|
47
|
-
"1.0.2",
|
48
|
-
"1.0.1",
|
49
|
-
"1.0.0",
|
50
|
-
],
|
51
|
-
"dependencies": [
|
52
|
-
"apache-airflow>=2.9.0",
|
53
|
-
"apache-airflow-providers-common-compat>=1.2.1",
|
54
|
-
"flask>=2.2,<2.3",
|
55
|
-
"flask-appbuilder==4.5.3",
|
56
|
-
"flask-login>=0.6.2",
|
57
|
-
"google-re2>=1.0",
|
58
|
-
"jmespath>=0.7.0",
|
59
|
-
],
|
60
|
-
"additional-extras": [{"name": "kerberos", "dependencies": ["kerberos>=1.3.0"]}],
|
61
|
-
"devel-dependencies": ["kerberos>=1.3.0"],
|
62
29
|
"config": {
|
63
30
|
"fab": {
|
64
31
|
"description": "This section contains configs specific to FAB provider.",
|
@@ -84,6 +51,34 @@ def get_provider_info():
|
|
84
51
|
"example": None,
|
85
52
|
"default": "True",
|
86
53
|
},
|
54
|
+
"auth_backends": {
|
55
|
+
"description": "Comma separated list of auth backends to authenticate users of the API.\n",
|
56
|
+
"version_added": "2.0.0",
|
57
|
+
"type": "string",
|
58
|
+
"example": None,
|
59
|
+
"default": "airflow.providers.fab.auth_manager.api.auth.backend.session",
|
60
|
+
},
|
61
|
+
"config_file": {
|
62
|
+
"description": "Path of webserver config file used for configuring the webserver parameters\n",
|
63
|
+
"version_added": "2.0.0",
|
64
|
+
"type": "string",
|
65
|
+
"example": None,
|
66
|
+
"default": "{AIRFLOW_HOME}/webserver_config.py",
|
67
|
+
},
|
68
|
+
"session_backend": {
|
69
|
+
"description": "The type of backend used to store web session data, can be ``database`` or ``securecookie``. For the\n``database`` backend, sessions are store in the database and they can be\nmanaged there (for example when you reset password of the user, all sessions for that user are\ndeleted). For the ``securecookie`` backend, sessions are stored in encrypted cookies on the client\nside. The ``securecookie`` mechanism is 'lighter' than database backend, but sessions are not\ndeleted when you reset password of the user, which means that other than waiting for expiry time,\nthe only way to invalidate all sessions for a user is to change secret_key and restart webserver\n(which also invalidates and logs out all other user's sessions).\n\nWhen you are using ``database`` backend, make sure to keep your database session table small\nby periodically running ``airflow db clean --table session`` command, especially if you have\nautomated API calls that will create a new session for each call rather than reuse the sessions\nstored in browser cookies.\n",
|
70
|
+
"version_added": "2.0.0",
|
71
|
+
"type": "string",
|
72
|
+
"example": "securecookie",
|
73
|
+
"default": "database",
|
74
|
+
},
|
75
|
+
"session_lifetime_minutes": {
|
76
|
+
"description": "The UI cookie lifetime in minutes. User will be logged out from UI after\n``[fab] session_lifetime_minutes`` of non-activity\n",
|
77
|
+
"version_added": "2.0.0",
|
78
|
+
"type": "integer",
|
79
|
+
"example": None,
|
80
|
+
"default": "43200",
|
81
|
+
},
|
87
82
|
},
|
88
83
|
}
|
89
84
|
},
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
from typing import TYPE_CHECKING, Any
|
20
|
+
|
21
|
+
from flask import Flask
|
22
|
+
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
from airflow.models.dagbag import DagBag
|
25
|
+
|
26
|
+
|
27
|
+
class AirflowApp(Flask):
|
28
|
+
"""Airflow Flask Application."""
|
29
|
+
|
30
|
+
dag_bag: DagBag
|
31
|
+
api_auth: list[Any]
|
@@ -0,0 +1,17 @@
|
|
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.
|
@@ -0,0 +1,197 @@
|
|
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 http import HTTPStatus
|
20
|
+
from typing import TYPE_CHECKING, Any
|
21
|
+
|
22
|
+
import werkzeug
|
23
|
+
from connexion import FlaskApi, ProblemException, problem
|
24
|
+
|
25
|
+
from airflow.utils.docs import get_docs_url
|
26
|
+
|
27
|
+
if TYPE_CHECKING:
|
28
|
+
import flask
|
29
|
+
|
30
|
+
doc_link = get_docs_url("stable-rest-api-ref.html")
|
31
|
+
|
32
|
+
EXCEPTIONS_LINK_MAP = {
|
33
|
+
400: f"{doc_link}#section/Errors/BadRequest",
|
34
|
+
404: f"{doc_link}#section/Errors/NotFound",
|
35
|
+
405: f"{doc_link}#section/Errors/MethodNotAllowed",
|
36
|
+
401: f"{doc_link}#section/Errors/Unauthenticated",
|
37
|
+
409: f"{doc_link}#section/Errors/AlreadyExists",
|
38
|
+
403: f"{doc_link}#section/Errors/PermissionDenied",
|
39
|
+
500: f"{doc_link}#section/Errors/Unknown",
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
def common_error_handler(exception: BaseException) -> flask.Response:
|
44
|
+
"""Use to capture connexion exceptions and add link to the type field."""
|
45
|
+
if isinstance(exception, ProblemException):
|
46
|
+
link = EXCEPTIONS_LINK_MAP.get(exception.status)
|
47
|
+
if link:
|
48
|
+
response = problem(
|
49
|
+
status=exception.status,
|
50
|
+
title=exception.title,
|
51
|
+
detail=exception.detail,
|
52
|
+
type=link,
|
53
|
+
instance=exception.instance,
|
54
|
+
headers=exception.headers,
|
55
|
+
ext=exception.ext,
|
56
|
+
)
|
57
|
+
else:
|
58
|
+
response = problem(
|
59
|
+
status=exception.status,
|
60
|
+
title=exception.title,
|
61
|
+
detail=exception.detail,
|
62
|
+
type=exception.type,
|
63
|
+
instance=exception.instance,
|
64
|
+
headers=exception.headers,
|
65
|
+
ext=exception.ext,
|
66
|
+
)
|
67
|
+
else:
|
68
|
+
if not isinstance(exception, werkzeug.exceptions.HTTPException):
|
69
|
+
exception = werkzeug.exceptions.InternalServerError()
|
70
|
+
|
71
|
+
response = problem(title=exception.name, detail=exception.description, status=exception.code)
|
72
|
+
|
73
|
+
return FlaskApi.get_response(response)
|
74
|
+
|
75
|
+
|
76
|
+
class NotFound(ProblemException):
|
77
|
+
"""Raise when the object cannot be found."""
|
78
|
+
|
79
|
+
def __init__(
|
80
|
+
self,
|
81
|
+
title: str = "Not Found",
|
82
|
+
detail: str | None = None,
|
83
|
+
headers: dict | None = None,
|
84
|
+
**kwargs: Any,
|
85
|
+
) -> None:
|
86
|
+
super().__init__(
|
87
|
+
status=HTTPStatus.NOT_FOUND,
|
88
|
+
type=EXCEPTIONS_LINK_MAP[404],
|
89
|
+
title=title,
|
90
|
+
detail=detail,
|
91
|
+
headers=headers,
|
92
|
+
**kwargs,
|
93
|
+
)
|
94
|
+
|
95
|
+
|
96
|
+
class BadRequest(ProblemException):
|
97
|
+
"""Raise when the server processes a bad request."""
|
98
|
+
|
99
|
+
def __init__(
|
100
|
+
self,
|
101
|
+
title: str = "Bad Request",
|
102
|
+
detail: str | None = None,
|
103
|
+
headers: dict | None = None,
|
104
|
+
**kwargs: Any,
|
105
|
+
) -> None:
|
106
|
+
super().__init__(
|
107
|
+
status=HTTPStatus.BAD_REQUEST,
|
108
|
+
type=EXCEPTIONS_LINK_MAP[400],
|
109
|
+
title=title,
|
110
|
+
detail=detail,
|
111
|
+
headers=headers,
|
112
|
+
**kwargs,
|
113
|
+
)
|
114
|
+
|
115
|
+
|
116
|
+
class Unauthenticated(ProblemException):
|
117
|
+
"""Raise when the user is not authenticated."""
|
118
|
+
|
119
|
+
def __init__(
|
120
|
+
self,
|
121
|
+
title: str = "Unauthorized",
|
122
|
+
detail: str | None = None,
|
123
|
+
headers: dict | None = None,
|
124
|
+
**kwargs: Any,
|
125
|
+
):
|
126
|
+
super().__init__(
|
127
|
+
status=HTTPStatus.UNAUTHORIZED,
|
128
|
+
type=EXCEPTIONS_LINK_MAP[401],
|
129
|
+
title=title,
|
130
|
+
detail=detail,
|
131
|
+
headers=headers,
|
132
|
+
**kwargs,
|
133
|
+
)
|
134
|
+
|
135
|
+
|
136
|
+
class PermissionDenied(ProblemException):
|
137
|
+
"""Raise when the user does not have the required permissions."""
|
138
|
+
|
139
|
+
def __init__(
|
140
|
+
self,
|
141
|
+
title: str = "Forbidden",
|
142
|
+
detail: str | None = None,
|
143
|
+
headers: dict | None = None,
|
144
|
+
**kwargs: Any,
|
145
|
+
) -> None:
|
146
|
+
super().__init__(
|
147
|
+
status=HTTPStatus.FORBIDDEN,
|
148
|
+
type=EXCEPTIONS_LINK_MAP[403],
|
149
|
+
title=title,
|
150
|
+
detail=detail,
|
151
|
+
headers=headers,
|
152
|
+
**kwargs,
|
153
|
+
)
|
154
|
+
|
155
|
+
|
156
|
+
class Conflict(ProblemException):
|
157
|
+
"""Raise when there is some conflict."""
|
158
|
+
|
159
|
+
def __init__(
|
160
|
+
self,
|
161
|
+
title="Conflict",
|
162
|
+
detail: str | None = None,
|
163
|
+
headers: dict | None = None,
|
164
|
+
**kwargs: Any,
|
165
|
+
):
|
166
|
+
super().__init__(
|
167
|
+
status=HTTPStatus.CONFLICT,
|
168
|
+
type=EXCEPTIONS_LINK_MAP[409],
|
169
|
+
title=title,
|
170
|
+
detail=detail,
|
171
|
+
headers=headers,
|
172
|
+
**kwargs,
|
173
|
+
)
|
174
|
+
|
175
|
+
|
176
|
+
class AlreadyExists(Conflict):
|
177
|
+
"""Raise when the object already exists."""
|
178
|
+
|
179
|
+
|
180
|
+
class Unknown(ProblemException):
|
181
|
+
"""Returns a response body and status code for HTTP 500 exception."""
|
182
|
+
|
183
|
+
def __init__(
|
184
|
+
self,
|
185
|
+
title: str = "Internal Server Error",
|
186
|
+
detail: str | None = None,
|
187
|
+
headers: dict | None = None,
|
188
|
+
**kwargs: Any,
|
189
|
+
) -> None:
|
190
|
+
super().__init__(
|
191
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
192
|
+
type=EXCEPTIONS_LINK_MAP[500],
|
193
|
+
title=title,
|
194
|
+
detail=detail,
|
195
|
+
headers=headers,
|
196
|
+
**kwargs,
|
197
|
+
)
|
@@ -0,0 +1,131 @@
|
|
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 logging
|
20
|
+
from collections.abc import Container
|
21
|
+
from functools import wraps
|
22
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
|
23
|
+
|
24
|
+
from pendulum.parsing import ParserError
|
25
|
+
from sqlalchemy import text
|
26
|
+
|
27
|
+
from airflow.configuration import conf
|
28
|
+
from airflow.providers.fab.www.api_connexion.exceptions import BadRequest
|
29
|
+
from airflow.utils import timezone
|
30
|
+
|
31
|
+
if TYPE_CHECKING:
|
32
|
+
from datetime import datetime
|
33
|
+
|
34
|
+
from sqlalchemy.sql import Select
|
35
|
+
|
36
|
+
log = logging.getLogger(__name__)
|
37
|
+
|
38
|
+
|
39
|
+
def validate_istimezone(value: datetime) -> None:
|
40
|
+
"""Validate that a datetime is not naive."""
|
41
|
+
if not value.tzinfo:
|
42
|
+
raise BadRequest("Invalid datetime format", detail="Naive datetime is disallowed")
|
43
|
+
|
44
|
+
|
45
|
+
def format_datetime(value: str) -> datetime:
|
46
|
+
"""
|
47
|
+
Format datetime objects.
|
48
|
+
|
49
|
+
Datetime format parser for args since connexion doesn't parse datetimes
|
50
|
+
https://github.com/zalando/connexion/issues/476
|
51
|
+
|
52
|
+
This should only be used within connection views because it raises 400
|
53
|
+
"""
|
54
|
+
value = value.strip()
|
55
|
+
if value[-1] != "Z":
|
56
|
+
value = value.replace(" ", "+")
|
57
|
+
try:
|
58
|
+
return timezone.parse(value)
|
59
|
+
except (ParserError, TypeError) as err:
|
60
|
+
raise BadRequest("Incorrect datetime argument", detail=str(err))
|
61
|
+
|
62
|
+
|
63
|
+
def check_limit(value: int) -> int:
|
64
|
+
"""
|
65
|
+
Check the limit does not exceed configured value.
|
66
|
+
|
67
|
+
This checks the limit passed to view and raises BadRequest if
|
68
|
+
limit exceed user configured value
|
69
|
+
"""
|
70
|
+
max_val = conf.getint("api", "maximum_page_limit") # user configured max page limit
|
71
|
+
fallback = conf.getint("api", "fallback_page_limit")
|
72
|
+
|
73
|
+
if value > max_val:
|
74
|
+
log.warning(
|
75
|
+
"The limit param value %s passed in API exceeds the configured maximum page limit %s",
|
76
|
+
value,
|
77
|
+
max_val,
|
78
|
+
)
|
79
|
+
return max_val
|
80
|
+
if value == 0:
|
81
|
+
return fallback
|
82
|
+
if value < 0:
|
83
|
+
raise BadRequest("Page limit must be a positive integer")
|
84
|
+
return value
|
85
|
+
|
86
|
+
|
87
|
+
T = TypeVar("T", bound=Callable)
|
88
|
+
|
89
|
+
|
90
|
+
def format_parameters(params_formatters: dict[str, Callable[[Any], Any]]) -> Callable[[T], T]:
|
91
|
+
"""
|
92
|
+
Create a decorator to convert parameters using given formatters.
|
93
|
+
|
94
|
+
Using it allows you to separate parameter formatting from endpoint logic.
|
95
|
+
|
96
|
+
:param params_formatters: Map of key name and formatter function
|
97
|
+
"""
|
98
|
+
|
99
|
+
def format_parameters_decorator(func: T) -> T:
|
100
|
+
@wraps(func)
|
101
|
+
def wrapped_function(*args, **kwargs):
|
102
|
+
for key, formatter in params_formatters.items():
|
103
|
+
if key in kwargs:
|
104
|
+
kwargs[key] = formatter(kwargs[key])
|
105
|
+
return func(*args, **kwargs)
|
106
|
+
|
107
|
+
return cast("T", wrapped_function)
|
108
|
+
|
109
|
+
return format_parameters_decorator
|
110
|
+
|
111
|
+
|
112
|
+
def apply_sorting(
|
113
|
+
query: Select,
|
114
|
+
order_by: str,
|
115
|
+
to_replace: dict[str, str] | None = None,
|
116
|
+
allowed_attrs: Container[str] | None = None,
|
117
|
+
) -> Select:
|
118
|
+
"""Apply sorting to query."""
|
119
|
+
lstriped_orderby = order_by.lstrip("-")
|
120
|
+
if allowed_attrs and lstriped_orderby not in allowed_attrs:
|
121
|
+
raise BadRequest(
|
122
|
+
detail=f"Ordering with '{lstriped_orderby}' is disallowed or "
|
123
|
+
f"the attribute does not exist on the model"
|
124
|
+
)
|
125
|
+
if to_replace:
|
126
|
+
lstriped_orderby = to_replace.get(lstriped_orderby, lstriped_orderby)
|
127
|
+
if order_by[0] == "-":
|
128
|
+
order_by = f"{lstriped_orderby} desc"
|
129
|
+
else:
|
130
|
+
order_by = f"{lstriped_orderby} asc"
|
131
|
+
return query.order_by(text(order_by))
|
@@ -0,0 +1,84 @@
|
|
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 functools import wraps
|
20
|
+
from typing import TYPE_CHECKING, Callable, TypeVar, cast
|
21
|
+
|
22
|
+
from flask import Response, current_app
|
23
|
+
|
24
|
+
from airflow.api_fastapi.app import get_auth_manager
|
25
|
+
from airflow.providers.fab.www.api_connexion.exceptions import PermissionDenied, Unauthenticated
|
26
|
+
|
27
|
+
if TYPE_CHECKING:
|
28
|
+
from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod
|
29
|
+
from airflow.providers.fab.www.airflow_flask_app import AirflowApp
|
30
|
+
|
31
|
+
T = TypeVar("T", bound=Callable)
|
32
|
+
|
33
|
+
|
34
|
+
def check_authentication() -> None:
|
35
|
+
"""Check that the request has valid authorization information."""
|
36
|
+
for auth in cast("AirflowApp", current_app).api_auth:
|
37
|
+
response = auth.requires_authentication(Response)()
|
38
|
+
if response.status_code == 200:
|
39
|
+
return
|
40
|
+
|
41
|
+
# since this handler only checks authentication, not authorization,
|
42
|
+
# we should always return 401
|
43
|
+
raise Unauthenticated(headers=response.headers)
|
44
|
+
|
45
|
+
|
46
|
+
def _requires_access(*, is_authorized_callback: Callable[[], bool], func: Callable, args, kwargs) -> bool:
|
47
|
+
"""
|
48
|
+
Define the behavior whether the user is authorized to access the resource.
|
49
|
+
|
50
|
+
:param is_authorized_callback: callback to execute to figure whether the user is authorized to access
|
51
|
+
the resource
|
52
|
+
:param func: the function to call if the user is authorized
|
53
|
+
:param args: the arguments of ``func``
|
54
|
+
:param kwargs: the keyword arguments ``func``
|
55
|
+
|
56
|
+
:meta private:
|
57
|
+
"""
|
58
|
+
check_authentication()
|
59
|
+
if is_authorized_callback():
|
60
|
+
return func(*args, **kwargs)
|
61
|
+
raise PermissionDenied()
|
62
|
+
|
63
|
+
|
64
|
+
def requires_access_custom_view(
|
65
|
+
method: ResourceMethod,
|
66
|
+
resource_name: str,
|
67
|
+
) -> Callable[[T], T]:
|
68
|
+
def requires_access_decorator(func: T):
|
69
|
+
@wraps(func)
|
70
|
+
def decorated(*args, **kwargs):
|
71
|
+
return _requires_access(
|
72
|
+
is_authorized_callback=lambda: get_auth_manager().is_authorized_custom_view(
|
73
|
+
method=method,
|
74
|
+
resource_name=resource_name,
|
75
|
+
user=get_auth_manager().get_user(),
|
76
|
+
),
|
77
|
+
func=func,
|
78
|
+
args=args,
|
79
|
+
kwargs=kwargs,
|
80
|
+
)
|
81
|
+
|
82
|
+
return cast("T", decorated)
|
83
|
+
|
84
|
+
return requires_access_decorator
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
from collections.abc import Mapping, Sequence
|
20
|
+
from typing import Any, Optional, Union
|
21
|
+
|
22
|
+
from flask import Response
|
23
|
+
|
24
|
+
APIResponse = Union[
|
25
|
+
Response,
|
26
|
+
tuple[object, int], # For '(NoContent, 201)'.
|
27
|
+
Mapping[str, Any], # JSON.
|
28
|
+
]
|
29
|
+
|
30
|
+
UpdateMask = Optional[Sequence[str]]
|