apache-airflow-providers-fab 1.5.3__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.
Files changed (101) hide show
  1. airflow/providers/fab/LICENSE +0 -52
  2. airflow/providers/fab/__init__.py +3 -3
  3. airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +3 -3
  4. airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +4 -4
  5. airflow/providers/fab/auth_manager/api/auth/backend/session.py +1 -1
  6. airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +14 -13
  7. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +12 -12
  8. airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
  9. airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
  10. airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
  11. airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
  12. airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +152 -0
  13. airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
  14. airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
  15. airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
  16. airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
  17. airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -4
  18. airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
  19. airflow/providers/fab/auth_manager/cli_commands/utils.py +10 -9
  20. airflow/providers/fab/auth_manager/fab_auth_manager.py +231 -126
  21. airflow/providers/fab/auth_manager/models/__init__.py +1 -1
  22. airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
  23. airflow/providers/fab/auth_manager/models/db.py +22 -5
  24. airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
  25. airflow/providers/fab/auth_manager/security_manager/override.py +71 -632
  26. airflow/providers/fab/auth_manager/views/permissions.py +1 -1
  27. airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
  28. airflow/providers/fab/auth_manager/views/user.py +1 -1
  29. airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
  30. airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
  31. airflow/providers/fab/get_provider_info.py +22 -16
  32. airflow/providers/fab/www/airflow_flask_app.py +31 -0
  33. airflow/providers/fab/www/api_connexion/__init__.py +17 -0
  34. airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
  35. airflow/providers/fab/www/api_connexion/parameters.py +131 -0
  36. airflow/providers/fab/www/api_connexion/security.py +84 -0
  37. airflow/providers/fab/www/api_connexion/types.py +30 -0
  38. airflow/providers/fab/www/app.py +112 -0
  39. airflow/providers/fab/www/auth.py +350 -0
  40. airflow/providers/fab/www/constants.py +28 -0
  41. airflow/providers/fab/www/extensions/__init__.py +16 -0
  42. airflow/providers/fab/www/extensions/init_appbuilder.py +602 -0
  43. airflow/providers/fab/www/extensions/init_jinja_globals.py +82 -0
  44. airflow/providers/fab/www/extensions/init_manifest_files.py +61 -0
  45. airflow/providers/fab/www/extensions/init_security.py +61 -0
  46. airflow/providers/fab/www/extensions/init_session.py +64 -0
  47. airflow/providers/fab/www/extensions/init_views.py +177 -0
  48. airflow/providers/fab/www/package-lock.json +10127 -0
  49. airflow/providers/fab/www/package.json +81 -0
  50. airflow/providers/fab/www/security/__init__.py +17 -0
  51. airflow/providers/fab/www/security/permissions.py +126 -0
  52. airflow/providers/fab/www/security_appless.py +44 -0
  53. airflow/providers/fab/www/security_manager.py +122 -0
  54. airflow/providers/fab/www/session.py +41 -0
  55. airflow/providers/fab/www/static/css/bootstrap-theme.css +6215 -0
  56. airflow/providers/fab/www/static/css/flash.css +57 -0
  57. airflow/providers/fab/www/static/css/loading-dots.css +60 -0
  58. airflow/providers/fab/www/static/css/main.css +676 -0
  59. airflow/providers/fab/www/static/css/material-icons.css +84 -0
  60. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
  61. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
  62. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
  63. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
  64. airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
  65. airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
  66. airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
  67. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
  68. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
  69. airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.css +18 -0
  70. airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.js +2 -0
  71. airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.js.LICENSE.txt +18 -0
  72. airflow/providers/fab/www/static/dist/manifest.json +17 -0
  73. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
  74. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
  75. airflow/providers/fab/www/static/dist/moment.4d28b37c229bdfc54575.js +2 -0
  76. airflow/providers/fab/www/static/dist/moment.4d28b37c229bdfc54575.js.LICENSE.txt +11 -0
  77. airflow/providers/fab/www/static/dist/oss-licenses.json +29 -0
  78. airflow/providers/fab/www/static/js/datetime_utils.js +134 -0
  79. airflow/providers/fab/www/static/js/main.js +324 -0
  80. airflow/providers/fab/www/static/sort_asc.png +0 -0
  81. airflow/providers/fab/www/static/sort_both.png +0 -0
  82. airflow/providers/fab/www/static/sort_desc.png +0 -0
  83. airflow/providers/fab/www/templates/airflow/_messages.html +30 -0
  84. airflow/providers/fab/www/templates/airflow/error.html +35 -0
  85. airflow/providers/fab/www/templates/airflow/main.html +78 -0
  86. airflow/providers/fab/www/templates/airflow/traceback.html +53 -0
  87. airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
  88. airflow/providers/fab/www/templates/appbuilder/index.html +20 -0
  89. airflow/providers/fab/www/templates/appbuilder/navbar.html +60 -0
  90. airflow/providers/fab/www/templates/appbuilder/navbar_menu.html +60 -0
  91. airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
  92. airflow/providers/fab/www/utils.py +272 -0
  93. airflow/providers/fab/www/views.py +129 -0
  94. airflow/providers/fab/www/webpack.config.js +213 -0
  95. {apache_airflow_providers_fab-1.5.3.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/METADATA +17 -35
  96. apache_airflow_providers_fab-2.0.0b1.dist-info/RECORD +122 -0
  97. {apache_airflow_providers_fab-1.5.3.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/WHEEL +1 -1
  98. airflow/providers/fab/auth_manager/decorators/auth.py +0 -126
  99. apache_airflow_providers_fab-1.5.3.dist-info/RECORD +0 -51
  100. /airflow/providers/fab/{auth_manager/decorators → www}/__init__.py +0 -0
  101. {apache_airflow_providers_fab-1.5.3.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -23,7 +23,7 @@ from flask_appbuilder.security.views import (
23
23
  )
24
24
  from flask_babel import lazy_gettext
25
25
 
26
- from airflow.security import permissions
26
+ from airflow.providers.fab.www.security import permissions
27
27
 
28
28
 
29
29
  class ActionModelView(PermissionModelView):
@@ -18,7 +18,7 @@ from __future__ import annotations
18
18
 
19
19
  from flask_appbuilder.security.views import RoleModelView
20
20
 
21
- from airflow.security import permissions
21
+ from airflow.providers.fab.www.security import permissions
22
22
 
23
23
 
24
24
  class CustomRoleModelView(RoleModelView):
@@ -28,7 +28,7 @@ from flask_appbuilder.security.views import (
28
28
  UserRemoteUserModelView,
29
29
  )
30
30
 
31
- from airflow.security import permissions
31
+ from airflow.providers.fab.www.security import permissions
32
32
 
33
33
 
34
34
  class MultiResourceUserMixin:
@@ -22,7 +22,7 @@ from flask_appbuilder.security.views import (
22
22
  UserInfoEditView,
23
23
  )
24
24
 
25
- from airflow.security import permissions
25
+ from airflow.providers.fab.www.security import permissions
26
26
 
27
27
 
28
28
  class CustomUserInfoEditView(UserInfoEditView):
@@ -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,10 +26,10 @@ 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,
29
+ "state": "not-ready",
30
+ "source-date-epoch": 1741121873,
32
31
  "versions": [
33
- "1.5.3",
32
+ "2.0.0b1",
34
33
  "1.5.2",
35
34
  "1.5.1",
36
35
  "1.5.0",
@@ -48,17 +47,6 @@ def get_provider_info():
48
47
  "1.0.1",
49
48
  "1.0.0",
50
49
  ],
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
50
  "config": {
63
51
  "fab": {
64
52
  "description": "This section contains configs specific to FAB provider.",
@@ -84,8 +72,26 @@ def get_provider_info():
84
72
  "example": None,
85
73
  "default": "True",
86
74
  },
75
+ "auth_backends": {
76
+ "description": "Comma separated list of auth backends to authenticate users of the API.\n",
77
+ "version_added": "2.3.0",
78
+ "type": "string",
79
+ "example": None,
80
+ "default": "airflow.providers.fab.auth_manager.api.auth.backend.session",
81
+ },
87
82
  },
88
83
  }
89
84
  },
90
85
  "auth-managers": ["airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager"],
86
+ "dependencies": [
87
+ "apache-airflow>=3.0.0.dev0",
88
+ "apache-airflow-providers-common-compat>=1.2.1",
89
+ "flask>=2.2,<2.3",
90
+ "flask-appbuilder==4.5.3",
91
+ "flask-login>=0.6.2",
92
+ "connexion[flask]>=2.14.2,<3.0",
93
+ "jmespath>=0.7.0",
94
+ ],
95
+ "optional-dependencies": {"kerberos": ["kerberos>=1.3.0"]},
96
+ "devel-dependencies": ["kerberos>=1.3.0"],
91
97
  }
@@ -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.airflow_flask_app import AirflowApp
26
+ from airflow.providers.fab.www.api_connexion.exceptions import PermissionDenied, Unauthenticated
27
+
28
+ if TYPE_CHECKING:
29
+ from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod
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]]