This is an automated email from the ASF dual-hosted git repository.
vincbeck pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 0201b1c640 Let auth managers provide their own API endpoints (#34349)
0201b1c640 is described below
commit 0201b1c640814f44a54e354d0f013fbb991758f3
Author: Raphaƫl Vandon <[email protected]>
AuthorDate: Wed Oct 18 00:10:12 2023 +0200
Let auth managers provide their own API endpoints (#34349)
---
MANIFEST.in | 1 +
.../endpoints/forward_to_fab_endpoint.py | 126 ++++
airflow/api_connexion/openapi/v1.yaml | 33 +-
airflow/auth/managers/base_auth_manager.py | 5 +
.../auth/managers/fab/api_endpoints/__init__.py | 26 -
.../api_endpoints}/role_and_permission_endpoint.py | 0
.../managers/fab/api_endpoints}/user_endpoint.py | 0
airflow/auth/managers/fab/fab_auth_manager.py | 22 +-
airflow/auth/managers/fab/openapi/v1.yaml | 700 +++++++++++++++++++++
airflow/www/app.py | 4 +
airflow/www/extensions/init_views.py | 34 +-
setup.cfg | 1 +
tests/api_connexion/conftest.py | 1 +
.../auth/managers/fab/api_endpoints/__init__.py | 26 -
.../managers/fab/api_endpoints}/conftest.py | 34 +-
.../test_role_and_permission_endpoint.py | 84 +--
.../fab/api_endpoints}/test_user_endpoint.py | 102 +--
tests/test_utils/decorators.py | 2 +
18 files changed, 1016 insertions(+), 185 deletions(-)
diff --git a/MANIFEST.in b/MANIFEST.in
index 983e6c09f4..5a636212b8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -32,6 +32,7 @@ exclude airflow/www/yarn.lock
exclude airflow/www/*.sh
include airflow/alembic.ini
include airflow/api_connexion/openapi/v1.yaml
+include airflow/auth/managers/fab/openapi/v1.yaml
include airflow/git_version
include airflow/provider_info.schema.json
include airflow/customized_form_field_behaviours.schema.json
diff --git a/airflow/api_connexion/endpoints/forward_to_fab_endpoint.py
b/airflow/api_connexion/endpoints/forward_to_fab_endpoint.py
new file mode 100644
index 0000000000..ded340d82a
--- /dev/null
+++ b/airflow/api_connexion/endpoints/forward_to_fab_endpoint.py
@@ -0,0 +1,126 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import warnings
+from typing import TYPE_CHECKING
+
+from airflow.api_connexion.exceptions import BadRequest
+from airflow.auth.managers.fab.api_endpoints import
role_and_permission_endpoint, user_endpoint
+from airflow.www.extensions.init_auth_manager import get_auth_manager
+
+if TYPE_CHECKING:
+ from typing import Callable
+
+ from airflow.api_connexion.types import APIResponse
+
+
+def _require_fab(func: Callable) -> Callable:
+ """
+ Raise an HTTP error 400 if the auth manager is not FAB.
+
+ Intended to decorate endpoints that have been migrated from Airflow API to
FAB API.
+ """
+
+ def inner(*args, **kwargs):
+ from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager
+
+ auth_mgr = get_auth_manager()
+ if not isinstance(auth_mgr, FabAuthManager):
+ raise BadRequest(
+ detail="This endpoint is only available when using the default
auth manager FabAuthManager."
+ )
+ else:
+ warnings.warn(
+ "This API endpoint is deprecated. "
+ "Please use the API under /auth/fab/v1 instead for this
operation.",
+ DeprecationWarning,
+ )
+ return func(*args, **kwargs)
+
+ return inner
+
+
+### role
+
+
+@_require_fab
+def get_role(**kwargs) -> APIResponse:
+ """Get role."""
+ return role_and_permission_endpoint.get_role(**kwargs)
+
+
+@_require_fab
+def get_roles(**kwargs) -> APIResponse:
+ """Get roles."""
+ return role_and_permission_endpoint.get_roles(**kwargs)
+
+
+@_require_fab
+def delete_role(**kwargs) -> APIResponse:
+ """Delete a role."""
+ return role_and_permission_endpoint.delete_role(**kwargs)
+
+
+@_require_fab
+def patch_role(**kwargs) -> APIResponse:
+ """Update a role."""
+ return role_and_permission_endpoint.patch_role(**kwargs)
+
+
+@_require_fab
+def post_role(**kwargs) -> APIResponse:
+ """Create a new role."""
+ return role_and_permission_endpoint.post_role(**kwargs)
+
+
+### permissions
+@_require_fab
+def get_permissions(**kwargs) -> APIResponse:
+ """Get permissions."""
+ return role_and_permission_endpoint.get_permissions(**kwargs)
+
+
+### user
+@_require_fab
+def get_user(**kwargs) -> APIResponse:
+ """Get a user."""
+ return user_endpoint.get_user(**kwargs)
+
+
+@_require_fab
+def get_users(**kwargs) -> APIResponse:
+ """Get users."""
+ return user_endpoint.get_users(**kwargs)
+
+
+@_require_fab
+def post_user(**kwargs) -> APIResponse:
+ """Create a new user."""
+ return user_endpoint.post_user(**kwargs)
+
+
+@_require_fab
+def patch_user(**kwargs) -> APIResponse:
+ """Update a user."""
+ return user_endpoint.patch_user(**kwargs)
+
+
+@_require_fab
+def delete_user(**kwargs) -> APIResponse:
+ """Delete a user."""
+ return user_endpoint.delete_user(**kwargs)
diff --git a/airflow/api_connexion/openapi/v1.yaml
b/airflow/api_connexion/openapi/v1.yaml
index d36e4a05a0..ebd10e855a 100644
--- a/airflow/api_connexion/openapi/v1.yaml
+++ b/airflow/api_connexion/openapi/v1.yaml
@@ -2127,12 +2127,13 @@ paths:
/roles:
get:
+ deprecated: true
summary: List roles
description: |
Get a list of roles.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.role_and_permission_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: get_roles
tags: [Role]
parameters:
@@ -2152,12 +2153,13 @@ paths:
$ref: '#/components/responses/PermissionDenied'
post:
+ deprecated: true
summary: Create a role
description: |
Create a new role.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.role_and_permission_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: post_role
tags: [Role]
requestBody:
@@ -2185,12 +2187,13 @@ paths:
- $ref: '#/components/parameters/RoleName'
get:
+ deprecated: true
summary: Get a role
description: |
Get a role.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.role_and_permission_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: get_role
tags: [Role]
responses:
@@ -2208,12 +2211,13 @@ paths:
$ref: '#/components/responses/NotFound'
patch:
+ deprecated: true
summary: Update a role
description: |
Update a role.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.role_and_permission_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: patch_role
tags: [Role]
parameters:
@@ -2242,12 +2246,13 @@ paths:
$ref: '#/components/responses/NotFound'
delete:
+ deprecated: true
summary: Delete a role
description: |
Delete a role.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.role_and_permission_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: delete_role
tags: [Role]
responses:
@@ -2264,12 +2269,13 @@ paths:
/permissions:
get:
+ deprecated: true
summary: List permissions
description: |
Get a list of permissions.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.role_and_permission_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: get_permissions
tags: [Permission]
parameters:
@@ -2289,12 +2295,13 @@ paths:
/users:
get:
+ deprecated: true
summary: List users
description: |
Get a list of users.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.user_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: get_users
tags: [User]
parameters:
@@ -2314,12 +2321,13 @@ paths:
$ref: '#/components/responses/PermissionDenied'
post:
+ deprecated: true
summary: Create a user
description: |
Create a new user with unique username and email.
*New in version 2.2.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.user_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: post_user
tags: [User]
requestBody:
@@ -2348,12 +2356,13 @@ paths:
parameters:
- $ref: '#/components/parameters/Username'
get:
+ deprecated: true
summary: Get a user
description: |
Get a user with a specific username.
*New in version 2.1.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.user_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: get_user
tags: [User]
responses:
@@ -2371,12 +2380,13 @@ paths:
$ref: '#/components/responses/NotFound'
patch:
+ deprecated: true
summary: Update a user
description: |
Update fields for a user.
*New in version 2.2.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.user_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: patch_user
tags: [User]
parameters:
@@ -2404,12 +2414,13 @@ paths:
$ref: '#/components/responses/NotFound'
delete:
+ deprecated: true
summary: Delete a user
description: |
Delete a user with a specific username.
*New in version 2.2.0*
- x-openapi-router-controller:
airflow.api_connexion.endpoints.user_endpoint
+ x-openapi-router-controller:
airflow.api_connexion.endpoints.forward_to_fab_endpoint
operationId: delete_user
tags: [User]
responses:
diff --git a/airflow/auth/managers/base_auth_manager.py
b/airflow/auth/managers/base_auth_manager.py
index 0700338069..2a5b1312a0 100644
--- a/airflow/auth/managers/base_auth_manager.py
+++ b/airflow/auth/managers/base_auth_manager.py
@@ -28,6 +28,7 @@ from airflow.utils.log.logging_mixin import LoggingMixin
from airflow.utils.session import NEW_SESSION, provide_session
if TYPE_CHECKING:
+ from connexion import FlaskApi
from flask import Flask
from sqlalchemy.orm import Session
@@ -66,6 +67,10 @@ class BaseAuthManager(LoggingMixin):
"""
return []
+ def get_api_endpoints(self) -> None | FlaskApi:
+ """Return API endpoint(s) definition for the auth manager."""
+ return None
+
@abstractmethod
def get_user_name(self) -> str:
"""Return the username associated to the user in session."""
diff --git a/MANIFEST.in b/airflow/auth/managers/fab/api_endpoints/__init__.py
similarity index 51%
copy from MANIFEST.in
copy to airflow/auth/managers/fab/api_endpoints/__init__.py
index 983e6c09f4..13a83393a9 100644
--- a/MANIFEST.in
+++ b/airflow/auth/managers/fab/api_endpoints/__init__.py
@@ -1,4 +1,3 @@
-#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
@@ -15,28 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-include NOTICE
-include LICENSE
-include RELEASE_NOTES.rst
-include README.md
-graft licenses
-graft airflow/www
-graft airflow/www/static
-graft airflow/www/templates
-graft scripts/systemd
-graft airflow/config_templates
-recursive-exclude airflow/www/node_modules *
-global-exclude __pycache__ *.pyc
-exclude airflow/www/yarn.lock
-exclude airflow/www/*.sh
-include airflow/alembic.ini
-include airflow/api_connexion/openapi/v1.yaml
-include airflow/git_version
-include airflow/provider_info.schema.json
-include airflow/customized_form_field_behaviours.schema.json
-include airflow/serialization/schema.json
-include airflow/utils/python_virtualenv_script.jinja2
-include airflow/utils/context.pyi
-include airflow/example_dags/sql/sample.sql
-include generated
diff --git a/airflow/api_connexion/endpoints/role_and_permission_endpoint.py
b/airflow/auth/managers/fab/api_endpoints/role_and_permission_endpoint.py
similarity index 100%
rename from airflow/api_connexion/endpoints/role_and_permission_endpoint.py
rename to
airflow/auth/managers/fab/api_endpoints/role_and_permission_endpoint.py
diff --git a/airflow/api_connexion/endpoints/user_endpoint.py
b/airflow/auth/managers/fab/api_endpoints/user_endpoint.py
similarity index 100%
rename from airflow/api_connexion/endpoints/user_endpoint.py
rename to airflow/auth/managers/fab/api_endpoints/user_endpoint.py
diff --git a/airflow/auth/managers/fab/fab_auth_manager.py
b/airflow/auth/managers/fab/fab_auth_manager.py
index 6c942babbf..1b98c33532 100644
--- a/airflow/auth/managers/fab/fab_auth_manager.py
+++ b/airflow/auth/managers/fab/fab_auth_manager.py
@@ -18,8 +18,10 @@
from __future__ import annotations
import warnings
+from pathlib import Path
from typing import TYPE_CHECKING, Container
+from connexion import FlaskApi
from flask import url_for
from sqlalchemy import select
from sqlalchemy.orm import Session, joinedload
@@ -43,6 +45,7 @@ from airflow.auth.managers.models.resource_details import (
from airflow.cli.cli_config import (
GroupCommand,
)
+from airflow.configuration import conf
from airflow.exceptions import AirflowException
from airflow.models import DagModel
from airflow.security import permissions
@@ -75,9 +78,10 @@ from airflow.security.permissions import (
RESOURCE_XCOM,
)
from airflow.utils.session import NEW_SESSION, provide_session
+from airflow.utils.yaml import safe_load
+from airflow.www.extensions.init_views import
_CustomErrorRequestBodyValidator, _LazyResolver
if TYPE_CHECKING:
-
from airflow.auth.managers.models.base_user import BaseUser
from airflow.cli.cli_config import (
CLICommand,
@@ -133,6 +137,22 @@ class FabAuthManager(BaseAuthManager):
SYNC_PERM_COMMAND, # not in a command group
]
+ def get_api_endpoints(self) -> None | FlaskApi:
+ folder = Path(__file__).parents[0].resolve() # this is
airflow/auth/managers/fab/
+ with folder.joinpath("openapi", "v1.yaml").open() as f:
+ specification = safe_load(f)
+ return FlaskApi(
+ specification=specification,
+ resolver=_LazyResolver(),
+ base_path="/auth/fab/v1",
+ options={
+ "swagger_ui": conf.getboolean("webserver",
"enable_swagger_ui", fallback=True),
+ },
+ strict_validation=True,
+ validate_responses=True,
+ validator_map={"body": _CustomErrorRequestBodyValidator},
+ )
+
def get_user_display_name(self) -> str:
"""Return the user's display name associated to the user in session."""
user = self.get_user()
diff --git a/airflow/auth/managers/fab/openapi/v1.yaml
b/airflow/auth/managers/fab/openapi/v1.yaml
new file mode 100644
index 0000000000..2c7239ae2e
--- /dev/null
+++ b/airflow/auth/managers/fab/openapi/v1.yaml
@@ -0,0 +1,700 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+openapi: 3.0.3
+
+info:
+ title: "Flask App Builder User & Role API"
+
+ version: '1.0.0'
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+ contact:
+ name: Apache Software Foundation
+ url: https://airflow.apache.org
+ email: [email protected]
+
+paths:
+ /roles:
+ get:
+ summary: List roles
+ description: |
+ Get a list of roles.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.role_and_permission_endpoint
+ operationId: get_roles
+ tags: [Role]
+ parameters:
+ - $ref: '#/components/parameters/PageLimit'
+ - $ref: '#/components/parameters/PageOffset'
+ - $ref: '#/components/parameters/OrderBy'
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RoleCollection'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+
+ post:
+ summary: Create a role
+ description: |
+ Create a new role.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.role_and_permission_endpoint
+ operationId: post_role
+ tags: [Role]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Role'
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Role'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+
+ /roles/{role_name}:
+ parameters:
+ - $ref: '#/components/parameters/RoleName'
+
+ get:
+ summary: Get a role
+ description: |
+ Get a role.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.role_and_permission_endpoint
+ operationId: get_role
+ tags: [Role]
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Role'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ patch:
+ summary: Update a role
+ description: |
+ Update a role.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.role_and_permission_endpoint
+ operationId: patch_role
+ tags: [Role]
+ parameters:
+ - $ref: '#/components/parameters/UpdateMask'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Role'
+
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Role'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ delete:
+ summary: Delete a role
+ description: |
+ Delete a role.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.role_and_permission_endpoint
+ operationId: delete_role
+ tags: [Role]
+ responses:
+ '204':
+ description: Success.
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ /permissions:
+ get:
+ summary: List permissions
+ description: |
+ Get a list of permissions.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.role_and_permission_endpoint
+ operationId: get_permissions
+ tags: [Permission]
+ parameters:
+ - $ref: '#/components/parameters/PageLimit'
+ - $ref: '#/components/parameters/PageOffset'
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ActionCollection'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+
+ /users:
+ get:
+ summary: List users
+ description: |
+ Get a list of users.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.user_endpoint
+ operationId: get_users
+ tags: [User]
+ parameters:
+ - $ref: '#/components/parameters/PageLimit'
+ - $ref: '#/components/parameters/PageOffset'
+ - $ref: '#/components/parameters/OrderBy'
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UserCollection'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+
+ post:
+ summary: Create a user
+ description: |
+ Create a new user with unique username and email.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.user_endpoint
+ operationId: post_user
+ tags: [User]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+ '409':
+ $ref: '#/components/responses/AlreadyExists'
+
+ /users/{username}:
+ parameters:
+ - $ref: '#/components/parameters/Username'
+ get:
+ summary: Get a user
+ description: |
+ Get a user with a specific username.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.user_endpoint
+ operationId: get_user
+ tags: [User]
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UserCollectionItem'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ patch:
+ summary: Update a user
+ description: |
+ Update fields for a user.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.user_endpoint
+ operationId: patch_user
+ tags: [User]
+ parameters:
+ - $ref: '#/components/parameters/UpdateMask'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ responses:
+ '200':
+ description: Success.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UserCollectionItem'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ delete:
+ summary: Delete a user
+ description: |
+ Delete a user with a specific username.
+
+ *New in version 2.8.0*
+ x-openapi-router-controller:
airflow.auth.managers.fab.api_endpoints.user_endpoint
+ operationId: delete_user
+ tags: [User]
+ responses:
+ '204':
+ description: Success.
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthenticated'
+ '403':
+ $ref: '#/components/responses/PermissionDenied'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+components:
+ # Reusable schemas (data models)
+ schemas:
+ # Database entities
+ UserCollectionItem:
+ description: |
+ A user object.
+
+ *New in version 2.8.0*
+ type: object
+ properties:
+ first_name:
+ type: string
+ description: |
+ The user's first name.
+ last_name:
+ type: string
+ description: |
+ The user's last name.
+ username:
+ type: string
+ description: |
+ The username.
+ minLength: 1
+ email:
+ type: string
+ description: |
+ The user's email.
+ minLength: 1
+ active:
+ type: boolean
+ description: Whether the user is active
+ readOnly: true
+ nullable: true
+ last_login:
+ type: string
+ format: datetime
+ description: The last user login
+ readOnly: true
+ nullable: true
+ login_count:
+ type: integer
+ description: The login count
+ readOnly: true
+ nullable: true
+ failed_login_count:
+ type: integer
+ description: The number of times the login failed
+ readOnly: true
+ nullable: true
+ roles:
+ type: array
+ description: |
+ User roles.
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ nullable: true
+ created_on:
+ type: string
+ format: datetime
+ description: The date user was created
+ readOnly: true
+ nullable: true
+ changed_on:
+ type: string
+ format: datetime
+ description: The date user was changed
+ readOnly: true
+ nullable: true
+ User:
+ type: object
+ description: |
+ A user object with sensitive data.
+
+ *New in version 2.8.0*
+ allOf:
+ - $ref: '#/components/schemas/UserCollectionItem'
+ - type: object
+ properties:
+ password:
+ type: string
+ writeOnly: true
+
+ UserCollection:
+ type: object
+ description: |
+ Collection of users.
+
+ *New in version 2.8.0*
+ allOf:
+ - type: object
+ properties:
+ users:
+ type: array
+ items:
+ $ref: '#/components/schemas/UserCollectionItem'
+ - $ref: '#/components/schemas/CollectionInfo'
+
+ Role:
+ description: |
+ a role item.
+
+ *New in version 2.8.0*
+ type: object
+ properties:
+ name:
+ type: string
+ description: |
+ The name of the role
+ minLength: 1
+ actions:
+ type: array
+ items:
+ $ref: '#/components/schemas/ActionResource'
+
+ RoleCollection:
+ description: |
+ A collection of roles.
+
+ *New in version 2.8.0*
+ type: object
+ allOf:
+ - type: object
+ properties:
+ roles:
+ type: array
+ items:
+ $ref: '#/components/schemas/Role'
+ - $ref: '#/components/schemas/CollectionInfo'
+
+ Action:
+ description: |
+ An action Item.
+
+ *New in version 2.8.0*
+ type: object
+ properties:
+ name:
+ type: string
+ description: The name of the permission "action"
+ nullable: false
+
+ ActionCollection:
+ description: |
+ A collection of actions.
+
+ *New in version 2.8.0*
+ type: object
+ allOf:
+ - type: object
+ properties:
+ actions:
+ type: array
+ items:
+ $ref: '#/components/schemas/Action'
+ - $ref: '#/components/schemas/CollectionInfo'
+
+ Resource:
+ description: |
+ A resource on which permissions are granted.
+
+ *New in version 2.8.0*
+ type: object
+ properties:
+ name:
+ type: string
+ description: The name of the resource
+ nullable: false
+
+ ActionResource:
+ description: |
+ The Action-Resource item.
+
+ *New in version 2.8.0*
+ type: object
+ properties:
+ action:
+ type: object
+ $ref: '#/components/schemas/Action'
+ description: The permission action
+ resource:
+ type: object
+ $ref: '#/components/schemas/Resource'
+ description: The permission resource
+
+ # Generic
+ Error:
+ description: |
+ [RFC7807](https://tools.ietf.org/html/rfc7807) compliant response.
+ type: object
+ properties:
+ type:
+ type: string
+ description: |
+ A URI reference [RFC3986] that identifies the problem type. This
specification
+ encourages that, when dereferenced, it provide human-readable
documentation for
+ the problem type.
+ title:
+ type: string
+ description: A short, human-readable summary of the problem type.
+ status:
+ type: number
+ description: The HTTP status code generated by the API server for
this occurrence of the problem.
+ detail:
+ type: string
+ description: A human-readable explanation specific to this
occurrence of the problem.
+ instance:
+ type: string
+ description: |
+ A URI reference that identifies the specific occurrence of the
problem. It may or may
+ not yield further information if dereferenced.
+ required:
+ - type
+ - title
+ - status
+
+ CollectionInfo:
+ description: Metadata about collection.
+ type: object
+ properties:
+ total_entries:
+ type: integer
+ description: |
+ Count of total objects in the current result set before pagination
parameters
+ (limit, offset) are applied.
+
+
+ # Reusable path, query, header and cookie parameters
+ parameters:
+ # Pagination parameters
+ PageOffset:
+ in: query
+ name: offset
+ required: false
+ schema:
+ type: integer
+ minimum: 0
+ description: The number of items to skip before starting to collect the
result set.
+
+ PageLimit:
+ in: query
+ name: limit
+ required: false
+ schema:
+ type: integer
+ default: 100
+ description: The numbers of items to return.
+
+ # Database entity fields
+ Username:
+ in: path
+ name: username
+ schema:
+ type: string
+ required: true
+ description: |
+ The username of the user.
+
+ *New in version 2.8.0*
+ RoleName:
+ in: path
+ name: role_name
+ schema:
+ type: string
+ required: true
+ description: The role name
+
+ OrderBy:
+ in: query
+ name: order_by
+ schema:
+ type: string
+ required: false
+ description: |
+ The name of the field to order the results by.
+ Prefix a field name with `-` to reverse the sort order.
+
+ *New in version 2.8.0*
+
+ UpdateMask:
+ in: query
+ name: update_mask
+ schema:
+ type: array
+ items:
+ type: string
+ description: |
+ The fields to update on the resource. If absent or empty, all
modifiable fields are updated.
+ A comma-separated list of fully qualified names of fields.
+ style: form
+ explode: false
+
+ # Reusable responses, such as 401 Unauthenticated or 400 Bad Request
+ responses:
+ # 400
+ 'BadRequest':
+ description: Client specified an invalid argument.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ # 401
+ 'Unauthenticated':
+ description: Request not authenticated due to missing, invalid,
authentication info.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ # 403
+ 'PermissionDenied':
+ description: Client does not have sufficient permission.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ # 404
+ 'NotFound':
+ description: A specified resource is not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ # 405
+ 'MethodNotAllowed':
+ description: Request method is known by the server but is not supported
by the target resource.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ # 406
+ 'NotAcceptable':
+ description: A specified Accept header is not allowed.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ # 409
+ 'AlreadyExists':
+ description: An existing resource conflicts with the request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ # 500
+ 'Unknown':
+ description: Unknown server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ securitySchemes:
+ Basic:
+ type: http
+ scheme: basic
+ GoogleOpenId:
+ type: openIdConnect
+ openIdConnectUrl:
https://accounts.google.com/.well-known/openid-configuration
+ Kerberos:
+ type: http
+ scheme: negotiate
+
+tags:
+ - name: Role
+ - name: Permission
+ - name: User
diff --git a/airflow/www/app.py b/airflow/www/app.py
index e8da104d22..ac6b87e79d 100644
--- a/airflow/www/app.py
+++ b/airflow/www/app.py
@@ -48,7 +48,9 @@ from airflow.www.extensions.init_security import (
)
from airflow.www.extensions.init_session import init_airflow_session_interface
from airflow.www.extensions.init_views import (
+ init_api_auth_provider,
init_api_connexion,
+ init_api_error_handlers,
init_api_experimental,
init_api_internal,
init_appbuilder_views,
@@ -169,6 +171,8 @@ def create_app(config=None, testing=False):
raise RuntimeError("The AIP_44 is not enabled so you cannot
use it.")
init_api_internal(flask_app)
init_api_experimental(flask_app)
+ init_api_auth_provider(flask_app)
+ init_api_error_handlers(flask_app) # needs to be after all api inits
to let them add their path first
sync_appbuilder_roles(flask_app)
diff --git a/airflow/www/extensions/init_views.py
b/airflow/www/extensions/init_views.py
index bb3c046081..e7ba0b72a1 100644
--- a/airflow/www/extensions/init_views.py
+++ b/airflow/www/extensions/init_views.py
@@ -33,6 +33,7 @@ from airflow.configuration import conf
from airflow.exceptions import RemovedInAirflow3Warning
from airflow.security import permissions
from airflow.utils.yaml import safe_load
+from airflow.www.extensions.init_auth_manager import get_auth_manager
if TYPE_CHECKING:
from flask import Flask
@@ -230,15 +231,16 @@ class
_CustomErrorRequestBodyValidator(RequestBodyValidator):
return super().validate_schema(data, url)
-def init_api_connexion(app: Flask) -> None:
- """Initialize Stable API."""
- base_path = "/api/v1"
+base_paths: list[str] = [] # contains the list of base paths that have api
endpoints
+
+def init_api_error_handlers(app: Flask) -> None:
+ """Add error handlers for 404 and 405 errors for existing API paths."""
from airflow.www import views
@app.errorhandler(404)
def _handle_api_not_found(ex):
- if request.path.startswith(base_path):
+ if any([request.path.startswith(p) for p in base_paths]):
# 404 errors are never handled on the blueprint level
# unless raised from a view func so actual 404 errors,
# i.e. "no route for it" defined, need to be handled
@@ -249,11 +251,19 @@ def init_api_connexion(app: Flask) -> None:
@app.errorhandler(405)
def _handle_method_not_allowed(ex):
- if request.path.startswith(base_path):
+ if any([request.path.startswith(p) for p in base_paths]):
return common_error_handler(ex)
else:
return views.method_not_allowed(ex)
+ app.register_error_handler(ProblemException, common_error_handler)
+
+
+def init_api_connexion(app: Flask) -> None:
+ """Initialize Stable API."""
+ base_path = "/api/v1"
+ base_paths.append(base_path)
+
with ROOT_APP_DIR.joinpath("api_connexion", "openapi", "v1.yaml").open()
as f:
specification = safe_load(f)
api_bp = FlaskApi(
@@ -271,7 +281,6 @@ def init_api_connexion(app: Flask) -> None:
api_bp.after_request(set_cors_headers_on_response)
app.register_blueprint(api_bp)
- app.register_error_handler(ProblemException, common_error_handler)
app.extensions["csrf"].exempt(api_bp)
@@ -280,6 +289,7 @@ def init_api_internal(app: Flask, standalone_api: bool =
False) -> None:
if not standalone_api and not conf.getboolean("webserver",
"run_internal_api", fallback=False):
return
+ base_paths.append("/internal_api/v1")
with ROOT_APP_DIR.joinpath("api_internal", "openapi",
"internal_api_v1.yaml").open() as f:
specification = safe_load(f)
api_bp = FlaskApi(
@@ -308,5 +318,17 @@ def init_api_experimental(app):
"The authenticated user has full access.",
RemovedInAirflow3Warning,
)
+ base_paths.append("/api/experimental")
app.register_blueprint(endpoints.api_experimental,
url_prefix="/api/experimental")
app.extensions["csrf"].exempt(endpoints.api_experimental)
+
+
+def init_api_auth_provider(app):
+ """Initialize the API offered by the auth manager."""
+ auth_mgr = get_auth_manager()
+ api = auth_mgr.get_api_endpoints()
+ if api:
+ blueprint = api.blueprint
+ base_paths.append(blueprint.url_prefix)
+ app.register_blueprint(blueprint)
+ app.extensions["csrf"].exempt(blueprint)
diff --git a/setup.cfg b/setup.cfg
index 09b400f6d8..b4adb9267c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -172,6 +172,7 @@ airflow=
provider_info.schema.json
airflow.api_connexion.openapi=*.yaml
+airflow.auth.managers.fab.openapi=*.yaml
airflow.serialization=*.json
airflow.utils=
context.pyi
diff --git a/tests/api_connexion/conftest.py b/tests/api_connexion/conftest.py
index e3865176dd..c860a78f27 100644
--- a/tests/api_connexion/conftest.py
+++ b/tests/api_connexion/conftest.py
@@ -33,6 +33,7 @@ def minimal_app_for_api():
"init_appbuilder",
"init_api_experimental_auth",
"init_api_connexion",
+ "init_api_error_handlers",
"init_airflow_session_interface",
"init_appbuilder_views",
]
diff --git a/MANIFEST.in b/tests/auth/managers/fab/api_endpoints/__init__.py
similarity index 51%
copy from MANIFEST.in
copy to tests/auth/managers/fab/api_endpoints/__init__.py
index 983e6c09f4..13a83393a9 100644
--- a/MANIFEST.in
+++ b/tests/auth/managers/fab/api_endpoints/__init__.py
@@ -1,4 +1,3 @@
-#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
@@ -15,28 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-include NOTICE
-include LICENSE
-include RELEASE_NOTES.rst
-include README.md
-graft licenses
-graft airflow/www
-graft airflow/www/static
-graft airflow/www/templates
-graft scripts/systemd
-graft airflow/config_templates
-recursive-exclude airflow/www/node_modules *
-global-exclude __pycache__ *.pyc
-exclude airflow/www/yarn.lock
-exclude airflow/www/*.sh
-include airflow/alembic.ini
-include airflow/api_connexion/openapi/v1.yaml
-include airflow/git_version
-include airflow/provider_info.schema.json
-include airflow/customized_form_field_behaviours.schema.json
-include airflow/serialization/schema.json
-include airflow/utils/python_virtualenv_script.jinja2
-include airflow/utils/context.pyi
-include airflow/example_dags/sql/sample.sql
-include generated
diff --git a/tests/api_connexion/conftest.py
b/tests/auth/managers/fab/api_endpoints/conftest.py
similarity index 60%
copy from tests/api_connexion/conftest.py
copy to tests/auth/managers/fab/api_endpoints/conftest.py
index e3865176dd..66707ef53d 100644
--- a/tests/api_connexion/conftest.py
+++ b/tests/auth/managers/fab/api_endpoints/conftest.py
@@ -16,25 +16,21 @@
# under the License.
from __future__ import annotations
-import warnings
-
import pytest
-from airflow.exceptions import RemovedInAirflow3Warning
from airflow.www import app
from tests.test_utils.config import conf_vars
from tests.test_utils.decorators import dont_initialize_flask_app_submodules
@pytest.fixture(scope="session")
-def minimal_app_for_api():
+def minimal_app_for_auth_api():
@dont_initialize_flask_app_submodules(
skip_all_except=[
"init_appbuilder",
"init_api_experimental_auth",
- "init_api_connexion",
- "init_airflow_session_interface",
- "init_appbuilder_views",
+ "init_api_auth_provider",
+ "init_api_error_handlers",
]
)
def factory():
@@ -42,27 +38,3 @@ def minimal_app_for_api():
return app.create_app(testing=True, config={"WTF_CSRF_ENABLED":
False}) # type:ignore
return factory()
-
-
[email protected]
-def session():
- from airflow.utils.session import create_session
-
- with create_session() as session:
- yield session
-
-
[email protected](scope="module")
-def dagbag():
- from airflow.models import DagBag
-
- with warnings.catch_warnings():
- # This explicitly shows off SubDagOperator, no point to warn about
that.
- warnings.filterwarnings(
- "ignore",
- category=RemovedInAirflow3Warning,
- message=r".+Please use.+TaskGroup.+",
- module=r".+example_subdag_operator$",
- )
- DagBag(include_examples=True, read_dags_from_db=False).sync_to_db()
- return DagBag(include_examples=True, read_dags_from_db=True)
diff --git a/tests/api_connexion/endpoints/test_role_and_permission_endpoint.py
b/tests/auth/managers/fab/api_endpoints/test_role_and_permission_endpoint.py
similarity index 86%
rename from tests/api_connexion/endpoints/test_role_and_permission_endpoint.py
rename to
tests/auth/managers/fab/api_endpoints/test_role_and_permission_endpoint.py
index bdede1f16f..b8a8d83699 100644
--- a/tests/api_connexion/endpoints/test_role_and_permission_endpoint.py
+++ b/tests/auth/managers/fab/api_endpoints/test_role_and_permission_endpoint.py
@@ -32,8 +32,8 @@ from tests.test_utils.api_connexion_utils import (
@pytest.fixture(scope="module")
-def configured_app(minimal_app_for_api):
- app = minimal_app_for_api
+def configured_app(minimal_app_for_auth_api):
+ app = minimal_app_for_auth_api
create_user(
app, # type: ignore
username="test",
@@ -74,12 +74,14 @@ class TestRoleEndpoint:
class TestGetRoleEndpoint(TestRoleEndpoint):
def test_should_response_200(self):
- response = self.client.get("/api/v1/roles/Admin",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/roles/Admin",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json["name"] == "Admin"
def test_should_respond_404(self):
- response = self.client.get("/api/v1/roles/invalid-role",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get(
+ "/auth/fab/v1/roles/invalid-role",
environ_overrides={"REMOTE_USER": "test"}
+ )
assert response.status_code == 404
assert {
"detail": "Role with name 'invalid-role' was not found",
@@ -89,19 +91,19 @@ class TestGetRoleEndpoint(TestRoleEndpoint):
} == response.json
def test_should_raises_401_unauthenticated(self):
- response = self.client.get("/api/v1/roles/Admin")
+ response = self.client.get("/auth/fab/v1/roles/Admin")
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.get(
- "/api/v1/roles/Admin", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
+ "/auth/fab/v1/roles/Admin", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
)
assert response.status_code == 403
class TestGetRolesEndpoint(TestRoleEndpoint):
def test_should_response_200(self):
- response = self.client.get("/api/v1/roles",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/roles",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
existing_roles = set(EXISTING_ROLES)
existing_roles.update(["Test", "TestNoPermissions"])
@@ -110,19 +112,21 @@ class TestGetRolesEndpoint(TestRoleEndpoint):
assert roles == existing_roles
def test_should_raises_401_unauthenticated(self):
- response = self.client.get("/api/v1/roles")
+ response = self.client.get("/auth/fab/v1/roles")
assert_401(response)
def test_should_raises_400_for_invalid_order_by(self):
response = self.client.get(
- "/api/v1/roles?order_by=invalid",
environ_overrides={"REMOTE_USER": "test"}
+ "/auth/fab/v1/roles?order_by=invalid",
environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 400
msg = "Ordering with 'invalid' is disallowed or the attribute does not
exist on the model"
assert response.json["detail"] == msg
def test_should_raise_403_forbidden(self):
- response = self.client.get("/api/v1/roles",
environ_overrides={"REMOTE_USER": "test_no_permissions"})
+ response = self.client.get(
+ "/auth/fab/v1/roles", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
+ )
assert response.status_code == 403
@@ -130,20 +134,20 @@ class
TestGetRolesEndpointPaginationandFilter(TestRoleEndpoint):
@pytest.mark.parametrize(
"url, expected_roles",
[
- ("/api/v1/roles?limit=1", ["Admin"]),
- ("/api/v1/roles?limit=2", ["Admin", "Op"]),
+ ("/auth/fab/v1/roles?limit=1", ["Admin"]),
+ ("/auth/fab/v1/roles?limit=2", ["Admin", "Op"]),
(
- "/api/v1/roles?offset=1",
+ "/auth/fab/v1/roles?offset=1",
["Op", "Public", "Test", "TestNoPermissions", "User",
"Viewer"],
),
(
- "/api/v1/roles?offset=0",
+ "/auth/fab/v1/roles?offset=0",
["Admin", "Op", "Public", "Test", "TestNoPermissions", "User",
"Viewer"],
),
- ("/api/v1/roles?limit=1&offset=2", ["Public"]),
- ("/api/v1/roles?limit=1&offset=1", ["Op"]),
+ ("/auth/fab/v1/roles?limit=1&offset=2", ["Public"]),
+ ("/auth/fab/v1/roles?limit=1&offset=1", ["Op"]),
(
- "/api/v1/roles?limit=2&offset=2",
+ "/auth/fab/v1/roles?limit=2&offset=2",
["Public", "Test"],
),
],
@@ -161,7 +165,7 @@ class
TestGetRolesEndpointPaginationandFilter(TestRoleEndpoint):
class TestGetPermissionsEndpoint(TestRoleEndpoint):
def test_should_response_200(self):
- response = self.client.get("/api/v1/permissions",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/permissions",
environ_overrides={"REMOTE_USER": "test"})
actions = {i[0] for i in self.app.appbuilder.sm.get_all_permissions()
if i}
assert response.status_code == 200
assert response.json["total_entries"] == len(actions)
@@ -169,12 +173,12 @@ class TestGetPermissionsEndpoint(TestRoleEndpoint):
assert actions == returned_actions
def test_should_raises_401_unauthenticated(self):
- response = self.client.get("/api/v1/permissions")
+ response = self.client.get("/auth/fab/v1/permissions")
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.get(
- "/api/v1/permissions", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
+ "/auth/fab/v1/permissions", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
)
assert response.status_code == 403
@@ -185,7 +189,9 @@ class TestPostRole(TestRoleEndpoint):
"name": "Test2",
"actions": [{"resource": {"name": "Connections"}, "action":
{"name": "can_create"}}],
}
- response = self.client.post("/api/v1/roles", json=payload,
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.post(
+ "/auth/fab/v1/roles", json=payload,
environ_overrides={"REMOTE_USER": "test"}
+ )
assert response.status_code == 200
role = self.app.appbuilder.sm.find_role("Test2")
assert role is not None
@@ -256,7 +262,9 @@ class TestPostRole(TestRoleEndpoint):
],
)
def test_post_should_respond_400_for_invalid_payload(self, payload,
error_message):
- response = self.client.post("/api/v1/roles", json=payload,
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.post(
+ "/auth/fab/v1/roles", json=payload,
environ_overrides={"REMOTE_USER": "test"}
+ )
assert response.status_code == 400
assert response.json == {
"detail": error_message,
@@ -270,7 +278,9 @@ class TestPostRole(TestRoleEndpoint):
"name": "Test",
"actions": [{"resource": {"name": "Connections"}, "action":
{"name": "can_create"}}],
}
- response = self.client.post("/api/v1/roles", json=payload,
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.post(
+ "/auth/fab/v1/roles", json=payload,
environ_overrides={"REMOTE_USER": "test"}
+ )
assert response.status_code == 409
assert response.json == {
"detail": "Role with name 'Test' already exists; please update
with the PATCH endpoint",
@@ -281,7 +291,7 @@ class TestPostRole(TestRoleEndpoint):
def test_should_raises_401_unauthenticated(self):
response = self.client.post(
- "/api/v1/roles",
+ "/auth/fab/v1/roles",
json={
"name": "Test2",
"actions": [{"resource": {"name": "Connections"}, "action":
{"name": "can_create"}}],
@@ -292,7 +302,7 @@ class TestPostRole(TestRoleEndpoint):
def test_should_raise_403_forbidden(self):
response = self.client.post(
- "/api/v1/roles",
+ "/auth/fab/v1/roles",
json={
"name": "mytest2",
"actions": [{"resource": {"name": "Connections"}, "action":
{"name": "can_create"}}],
@@ -305,14 +315,16 @@ class TestPostRole(TestRoleEndpoint):
class TestDeleteRole(TestRoleEndpoint):
def test_delete_should_respond_204(self, session):
role = create_role(self.app, "mytestrole")
- response = self.client.delete(f"/api/v1/roles/{role.name}",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.delete(
+ f"/auth/fab/v1/roles/{role.name}",
environ_overrides={"REMOTE_USER": "test"}
+ )
assert response.status_code == 204
role_obj = session.query(Role).filter(Role.name == role.name).all()
assert len(role_obj) == 0
def test_delete_should_respond_404(self):
response = self.client.delete(
- "/api/v1/roles/invalidrolename", environ_overrides={"REMOTE_USER":
"test"}
+ "/auth/fab/v1/roles/invalidrolename",
environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 404
assert response.json == {
@@ -323,13 +335,13 @@ class TestDeleteRole(TestRoleEndpoint):
}
def test_should_raises_401_unauthenticated(self):
- response = self.client.delete("/api/v1/roles/test")
+ response = self.client.delete("/auth/fab/v1/roles/test")
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.delete(
- "/api/v1/roles/test", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
+ "/auth/fab/v1/roles/test", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
)
assert response.status_code == 403
@@ -352,7 +364,7 @@ class TestPatchRole(TestRoleEndpoint):
def test_patch_should_respond_200(self, payload, expected_name,
expected_actions):
role = create_role(self.app, "mytestrole")
response = self.client.patch(
- f"/api/v1/roles/{role.name}", json=payload,
environ_overrides={"REMOTE_USER": "test"}
+ f"/auth/fab/v1/roles/{role.name}", json=payload,
environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json["name"] == expected_name
@@ -363,7 +375,7 @@ class TestPatchRole(TestRoleEndpoint):
create_role(self.app, "already_exists")
response = self.client.patch(
- "/api/v1/roles/role_to_change",
+ "/auth/fab/v1/roles/role_to_change",
json={
"name": "already_exists",
"actions": [{"action": {"name": "can_delete"}, "resource":
{"name": "XComs"}}],
@@ -408,7 +420,7 @@ class TestPatchRole(TestRoleEndpoint):
role = create_role(self.app, "mytestrole")
assert role.permissions == []
response = self.client.patch(
- f"/api/v1/roles/{role.name}{update_mask}",
+ f"/auth/fab/v1/roles/{role.name}{update_mask}",
json=payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -420,7 +432,7 @@ class TestPatchRole(TestRoleEndpoint):
role = create_role(self.app, "mytestrole")
payload = {"name": "testme"}
response = self.client.patch(
- f"/api/v1/roles/{role.name}?update_mask=invalid_name",
+ f"/auth/fab/v1/roles/{role.name}?update_mask=invalid_name",
json=payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -480,7 +492,7 @@ class TestPatchRole(TestRoleEndpoint):
def test_patch_should_respond_400_for_invalid_update(self, payload,
expected_error):
role = create_role(self.app, "mytestrole")
response = self.client.patch(
- f"/api/v1/roles/{role.name}",
+ f"/auth/fab/v1/roles/{role.name}",
json=payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -489,7 +501,7 @@ class TestPatchRole(TestRoleEndpoint):
def test_should_raises_401_unauthenticated(self):
response = self.client.patch(
- "/api/v1/roles/test",
+ "/auth/fab/v1/roles/test",
json={
"name": "mytest2",
"actions": [{"resource": {"name": "Connections"}, "action":
{"name": "can_create"}}],
@@ -500,7 +512,7 @@ class TestPatchRole(TestRoleEndpoint):
def test_should_raise_403_forbidden(self):
response = self.client.patch(
- "/api/v1/roles/test",
+ "/auth/fab/v1/roles/test",
json={
"name": "mytest2",
"actions": [{"resource": {"name": "Connections"}, "action":
{"name": "can_create"}}],
diff --git a/tests/api_connexion/endpoints/test_user_endpoint.py
b/tests/auth/managers/fab/api_endpoints/test_user_endpoint.py
similarity index 89%
rename from tests/api_connexion/endpoints/test_user_endpoint.py
rename to tests/auth/managers/fab/api_endpoints/test_user_endpoint.py
index ac0c48f689..51427ddfcb 100644
--- a/tests/api_connexion/endpoints/test_user_endpoint.py
+++ b/tests/auth/managers/fab/api_endpoints/test_user_endpoint.py
@@ -33,8 +33,8 @@ DEFAULT_TIME = "2020-06-11T18:00:00+00:00"
@pytest.fixture(scope="module")
-def configured_app(minimal_app_for_api):
- app = minimal_app_for_api
+def configured_app(minimal_app_for_auth_api):
+ app = minimal_app_for_auth_api
create_user(
app, # type: ignore
username="test",
@@ -91,7 +91,7 @@ class TestGetUser(TestUserEndpoint):
users = self._create_users(1)
self.session.add_all(users)
self.session.commit()
- response = self.client.get("/api/v1/users/TEST_USER1",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users/TEST_USER1",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json == {
"active": None,
@@ -119,7 +119,7 @@ class TestGetUser(TestUserEndpoint):
)
self.session.add_all([prince])
self.session.commit()
- response = self.client.get("/api/v1/users/prince",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users/prince",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json == {
"active": None,
@@ -147,7 +147,7 @@ class TestGetUser(TestUserEndpoint):
)
self.session.add_all([liberace])
self.session.commit()
- response = self.client.get("/api/v1/users/liberace",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users/liberace",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json == {
"active": None,
@@ -175,7 +175,7 @@ class TestGetUser(TestUserEndpoint):
)
self.session.add_all([nameless])
self.session.commit()
- response = self.client.get("/api/v1/users/nameless",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users/nameless",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json == {
"active": None,
@@ -192,7 +192,9 @@ class TestGetUser(TestUserEndpoint):
}
def test_should_respond_404(self):
- response = self.client.get("/api/v1/users/invalid-user",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get(
+ "/auth/fab/v1/users/invalid-user",
environ_overrides={"REMOTE_USER": "test"}
+ )
assert response.status_code == 404
assert {
"detail": "The User with username `invalid-user` was not found",
@@ -202,30 +204,32 @@ class TestGetUser(TestUserEndpoint):
} == response.json
def test_should_raises_401_unauthenticated(self):
- response = self.client.get("/api/v1/users/TEST_USER1")
+ response = self.client.get("/auth/fab/v1/users/TEST_USER1")
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.get(
- "/api/v1/users/TEST_USER1", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
+ "/auth/fab/v1/users/TEST_USER1", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
)
assert response.status_code == 403
class TestGetUsers(TestUserEndpoint):
def test_should_response_200(self):
- response = self.client.get("/api/v1/users",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json["total_entries"] == 2
usernames = [user["username"] for user in response.json["users"] if
user]
assert usernames == ["test", "test_no_permissions"]
def test_should_raises_401_unauthenticated(self):
- response = self.client.get("/api/v1/users")
+ response = self.client.get("/auth/fab/v1/users")
assert_401(response)
def test_should_raise_403_forbidden(self):
- response = self.client.get("/api/v1/users",
environ_overrides={"REMOTE_USER": "test_no_permissions"})
+ response = self.client.get(
+ "/auth/fab/v1/users", environ_overrides={"REMOTE_USER":
"test_no_permissions"}
+ )
assert response.status_code == 403
@@ -233,10 +237,10 @@ class TestGetUsersPagination(TestUserEndpoint):
@pytest.mark.parametrize(
"url, expected_usernames",
[
- ("/api/v1/users?limit=1", ["test"]),
- ("/api/v1/users?limit=2", ["test", "test_no_permissions"]),
+ ("/auth/fab/v1/users?limit=1", ["test"]),
+ ("/auth/fab/v1/users?limit=2", ["test", "test_no_permissions"]),
(
- "/api/v1/users?offset=5",
+ "/auth/fab/v1/users?offset=5",
[
"TEST_USER4",
"TEST_USER5",
@@ -248,7 +252,7 @@ class TestGetUsersPagination(TestUserEndpoint):
],
),
(
- "/api/v1/users?offset=0",
+ "/auth/fab/v1/users?offset=0",
[
"test",
"test_no_permissions",
@@ -264,10 +268,10 @@ class TestGetUsersPagination(TestUserEndpoint):
"TEST_USER10",
],
),
- ("/api/v1/users?limit=1&offset=5", ["TEST_USER4"]),
- ("/api/v1/users?limit=1&offset=1", ["test_no_permissions"]),
+ ("/auth/fab/v1/users?limit=1&offset=5", ["TEST_USER4"]),
+ ("/auth/fab/v1/users?limit=1&offset=1", ["test_no_permissions"]),
(
- "/api/v1/users?limit=2&offset=2",
+ "/auth/fab/v1/users?limit=2&offset=2",
["TEST_USER1", "TEST_USER2"],
),
],
@@ -287,7 +291,7 @@ class TestGetUsersPagination(TestUserEndpoint):
self.session.add_all(users)
self.session.commit()
- response = self.client.get("/api/v1/users",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
# Explicitly add the 2 users on setUp
assert response.json["total_entries"] == 200 + len(["test",
"test_no_permissions"])
@@ -297,7 +301,9 @@ class TestGetUsersPagination(TestUserEndpoint):
users = self._create_users(2)
self.session.add_all(users)
self.session.commit()
- response = self.client.get("/api/v1/users?order_by=myname",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get(
+ "/auth/fab/v1/users?order_by=myname",
environ_overrides={"REMOTE_USER": "test"}
+ )
assert response.status_code == 400
msg = "Ordering with 'myname' is disallowed or the attribute does not
exist on the model"
assert response.json["detail"] == msg
@@ -307,7 +313,7 @@ class TestGetUsersPagination(TestUserEndpoint):
self.session.add_all(users)
self.session.commit()
- response = self.client.get("/api/v1/users?limit=0",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users?limit=0",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
# Explicit add the 2 users on setUp
assert response.json["total_entries"] == 200 + len(["test",
"test_no_permissions"])
@@ -319,7 +325,7 @@ class TestGetUsersPagination(TestUserEndpoint):
self.session.add_all(users)
self.session.commit()
- response = self.client.get("/api/v1/users?limit=180",
environ_overrides={"REMOTE_USER": "test"})
+ response = self.client.get("/auth/fab/v1/users?limit=180",
environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert len(response.json["users"]) == 150
@@ -411,7 +417,7 @@ def autoclean_admin_user(configured_app,
autoclean_user_payload):
class TestPostUser(TestUserEndpoint):
def test_with_default_role(self, autoclean_username,
autoclean_user_payload):
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -424,7 +430,7 @@ class TestPostUser(TestUserEndpoint):
def test_with_custom_roles(self, autoclean_username,
autoclean_user_payload):
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json={"roles": [{"name": "User"}, {"name": "Viewer"}],
**autoclean_user_payload},
environ_overrides={"REMOTE_USER": "test"},
)
@@ -438,7 +444,7 @@ class TestPostUser(TestUserEndpoint):
@pytest.mark.usefixtures("user_different")
def test_with_existing_different_user(self, autoclean_user_payload):
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json={"roles": [{"name": "User"}, {"name": "Viewer"}],
**autoclean_user_payload},
environ_overrides={"REMOTE_USER": "test"},
)
@@ -446,14 +452,14 @@ class TestPostUser(TestUserEndpoint):
def test_unauthenticated(self, autoclean_user_payload):
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json=autoclean_user_payload,
)
assert response.status_code == 401, response.json
def test_forbidden(self, autoclean_user_payload):
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test_no_permissions"},
)
@@ -477,7 +483,7 @@ class TestPostUser(TestUserEndpoint):
existing = request.getfixturevalue(existing_user_fixture_name)
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -513,7 +519,7 @@ class TestPostUser(TestUserEndpoint):
)
def test_invalid_payload(self, autoclean_user_payload, payload_converter,
error_message):
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json=payload_converter(autoclean_user_payload),
environ_overrides={"REMOTE_USER": "test"},
)
@@ -528,7 +534,7 @@ class TestPostUser(TestUserEndpoint):
def test_internal_server_error(self, autoclean_user_payload):
with unittest.mock.patch.object(self.app.appbuilder.sm, "add_user",
return_value=None):
response = self.client.post(
- "/api/v1/users",
+ "/auth/fab/v1/users",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -545,7 +551,7 @@ class TestPatchUser(TestUserEndpoint):
def test_change(self, autoclean_username, autoclean_user_payload):
autoclean_user_payload["first_name"] = "Changed"
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -561,7 +567,7 @@ class TestPatchUser(TestUserEndpoint):
autoclean_user_payload["first_name"] = "Changed"
autoclean_user_payload["last_name"] = "McTesterson"
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}?update_mask=last_name",
+ f"/auth/fab/v1/users/{autoclean_username}?update_mask=last_name",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -591,7 +597,7 @@ class TestPatchUser(TestUserEndpoint):
):
autoclean_user_payload.update(payload)
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -612,7 +618,7 @@ class TestPatchUser(TestUserEndpoint):
):
autoclean_user_payload.pop(field)
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -624,7 +630,7 @@ class TestPatchUser(TestUserEndpoint):
testusername = "testusername"
autoclean_user_payload.update({"username": testusername})
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -633,7 +639,7 @@ class TestPatchUser(TestUserEndpoint):
@pytest.mark.usefixtures("autoclean_admin_user")
@unittest.mock.patch(
- "airflow.api_connexion.endpoints.user_endpoint.generate_password_hash",
+
"airflow.auth.managers.fab.api_endpoints.user_endpoint.generate_password_hash",
return_value="fake-hashed-pass",
)
def test_password_hashed(
@@ -644,7 +650,7 @@ class TestPatchUser(TestUserEndpoint):
):
autoclean_user_payload["password"] = "new-pass"
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -663,7 +669,7 @@ class TestPatchUser(TestUserEndpoint):
# Patching a user's roles should replace the entire list.
autoclean_user_payload["roles"] = [{"name": "User"}, {"name":
"Viewer"}]
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}?update_mask=roles",
+ f"/auth/fab/v1/users/{autoclean_username}?update_mask=roles",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -674,7 +680,7 @@ class TestPatchUser(TestUserEndpoint):
def test_unchanged(self, autoclean_username, autoclean_user_payload):
# Should allow a PATCH that changes nothing.
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -686,7 +692,7 @@ class TestPatchUser(TestUserEndpoint):
@pytest.mark.usefixtures("autoclean_admin_user")
def test_unauthenticated(self, autoclean_username, autoclean_user_payload):
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
)
assert response.status_code == 401, response.json
@@ -694,7 +700,7 @@ class TestPatchUser(TestUserEndpoint):
@pytest.mark.usefixtures("autoclean_admin_user")
def test_forbidden(self, autoclean_username, autoclean_user_payload):
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test_no_permissions"},
)
@@ -703,7 +709,7 @@ class TestPatchUser(TestUserEndpoint):
def test_not_found(self, autoclean_username, autoclean_user_payload):
# This test does not populate autoclean_admin_user into the database.
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=autoclean_user_payload,
environ_overrides={"REMOTE_USER": "test"},
)
@@ -743,7 +749,7 @@ class TestPatchUser(TestUserEndpoint):
error_message,
):
response = self.client.patch(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
json=payload_converter(autoclean_user_payload),
environ_overrides={"REMOTE_USER": "test"},
)
@@ -760,7 +766,7 @@ class TestDeleteUser(TestUserEndpoint):
@pytest.mark.usefixtures("autoclean_admin_user")
def test_delete(self, autoclean_username):
response = self.client.delete(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 204, response.json # NO CONTENT.
@@ -769,7 +775,7 @@ class TestDeleteUser(TestUserEndpoint):
@pytest.mark.usefixtures("autoclean_admin_user")
def test_unauthenticated(self, autoclean_username):
response = self.client.delete(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
)
assert response.status_code == 401, response.json
assert self.session.query(count(User.id)).filter(User.username ==
autoclean_username).scalar() == 1
@@ -777,7 +783,7 @@ class TestDeleteUser(TestUserEndpoint):
@pytest.mark.usefixtures("autoclean_admin_user")
def test_forbidden(self, autoclean_username):
response = self.client.delete(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
environ_overrides={"REMOTE_USER": "test_no_permissions"},
)
assert response.status_code == 403, response.json
@@ -786,7 +792,7 @@ class TestDeleteUser(TestUserEndpoint):
def test_not_found(self, autoclean_username):
# This test does not populate autoclean_admin_user into the database.
response = self.client.delete(
- f"/api/v1/users/{autoclean_username}",
+ f"/auth/fab/v1/users/{autoclean_username}",
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 404, response.json
diff --git a/tests/test_utils/decorators.py b/tests/test_utils/decorators.py
index 522f80a254..9cabbcc0cd 100644
--- a/tests/test_utils/decorators.py
+++ b/tests/test_utils/decorators.py
@@ -40,6 +40,8 @@ def dont_initialize_flask_app_submodules(_func=None, *,
skip_all_except=None):
"init_api_connexion",
"init_api_internal",
"init_api_experimental",
+ "init_api_auth_provider",
+ "init_api_error_handlers",
"sync_appbuilder_roles",
"init_jinja_globals",
"init_xframe_protection",