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",

Reply via email to