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 fa259279e92 Migrate FAB DELETE /roles to FastAPI (#57780)
fa259279e92 is described below
commit fa259279e92b6d2ac6dccde5283d9185177a93bd
Author: kyounghoonJang <[email protected]>
AuthorDate: Fri Nov 7 00:23:44 2025 +0900
Migrate FAB DELETE /roles to FastAPI (#57780)
---
.../openapi/v2-fab-auth-manager-generated.yaml | 48 +++++++++++++
.../fab/auth_manager/api_fastapi/routes/roles.py | 19 ++++-
.../fab/auth_manager/api_fastapi/services/roles.py | 12 ++++
.../auth_manager/api_fastapi/routes/test_roles.py | 80 ++++++++++++++++++++++
.../api_fastapi/services/test_roles.py | 20 ++++++
5 files changed, 178 insertions(+), 1 deletion(-)
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
index 565e2e3d308..ac496dfa556 100644
---
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
+++
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
@@ -215,6 +215,54 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ /auth/fab/v1/roles/{name}:
+ delete:
+ tags:
+ - FabAuthManager
+ summary: Delete Role
+ description: Delete an existing role.
+ operationId: delete_role
+ security:
+ - OAuth2PasswordBearer: []
+ - HTTPBearer: []
+ parameters:
+ - name: name
+ in: path
+ required: true
+ schema:
+ type: string
+ minLength: 1
+ title: Name
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '401':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Unauthorized
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Forbidden
+ '404':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Not Found
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
ActionResourceResponse:
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/roles.py
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/roles.py
index d2223e0e8de..e733b1bbc83 100644
---
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/roles.py
+++
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/roles.py
@@ -18,7 +18,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
-from fastapi import Depends, Query, status
+from fastapi import Depends, Path, Query, status
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
@@ -83,3 +83,20 @@ def get_roles(
"""List roles with pagination and ordering."""
with get_application_builder():
return FABAuthManagerRoles.get_roles(order_by=order_by, limit=limit,
offset=offset)
+
+
+@roles_router.delete(
+ "/roles/{name}",
+ responses=create_openapi_http_exception_doc(
+ [
+ status.HTTP_401_UNAUTHORIZED,
+ status.HTTP_403_FORBIDDEN,
+ status.HTTP_404_NOT_FOUND,
+ ]
+ ),
+ dependencies=[Depends(requires_fab_custom_view("DELETE",
permissions.RESOURCE_ROLE))],
+)
+def delete_role(name: str = Path(..., min_length=1)) -> None:
+ """Delete an existing role."""
+ with get_application_builder():
+ return FABAuthManagerRoles.delete_role(name=name)
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
index 3a3ae6b5c2d..350a99185f8 100644
---
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
+++
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
@@ -96,3 +96,15 @@ class FABAuthManagerRoles:
roles=[RoleResponse.model_validate(r) for r in roles],
total_entries=total_entries,
)
+
+ @classmethod
+ def delete_role(cls, name: str) -> None:
+ security_manager = get_fab_auth_manager().security_manager
+
+ existing = security_manager.find_role(name=name)
+ if not existing:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"Role with name {name!r} does not exist.",
+ )
+ security_manager.delete_role(existing)
diff --git
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_roles.py
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_roles.py
index a2deb8bd9c7..97d0f134596 100644
--- a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_roles.py
+++ b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_roles.py
@@ -21,6 +21,7 @@ from contextlib import nullcontext as _noop_cm
from unittest.mock import MagicMock, patch
import pytest
+from fastapi import HTTPException, status
from airflow.providers.fab.auth_manager.api_fastapi.datamodels.roles import (
RoleCollectionResponse,
@@ -252,3 +253,82 @@ class TestRoles:
resp = test_client.get("/fab/v1/roles", params={"offset": -1})
assert resp.status_code == 422
mock_roles.get_roles.assert_not_called()
+
+ # DELETE /roles/{name}
+
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.roles.FABAuthManagerRoles")
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.security.get_auth_manager")
+ @patch(
+
"airflow.providers.fab.auth_manager.api_fastapi.routes.roles.get_application_builder",
+ return_value=_noop_cm(),
+ )
+ def test_delete_role(
+ self, mock_get_application_builder, mock_get_auth_manager, mock_roles,
test_client, as_user
+ ):
+ mgr = MagicMock()
+ mgr.is_authorized_custom_view.return_value = True
+ mock_roles.delete_role.return_value = None
+ mock_get_auth_manager.return_value = mgr
+
+ with as_user():
+ resp = test_client.delete("/fab/v1/roles/roleA")
+ assert resp.status_code == 200
+ mock_roles.delete_role.assert_called_once_with(name="roleA")
+
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.roles.FABAuthManagerRoles")
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.security.get_auth_manager")
+ @patch(
+
"airflow.providers.fab.auth_manager.api_fastapi.routes.roles.get_application_builder",
+ return_value=_noop_cm(),
+ )
+ def test_delete_role_forbidden(
+ self, mock_get_application_builder, mock_get_auth_manager, mock_roles,
test_client, as_user
+ ):
+ mgr = MagicMock()
+ mgr.is_authorized_custom_view.return_value = False
+ mock_get_auth_manager.return_value = mgr
+
+ with as_user():
+ resp = test_client.delete("/fab/v1/roles/roleA")
+ assert resp.status_code == 403
+ mock_roles.delete_role.assert_not_called()
+
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.roles.FABAuthManagerRoles")
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.security.get_auth_manager")
+ @patch(
+
"airflow.providers.fab.auth_manager.api_fastapi.routes.roles.get_application_builder",
+ return_value=_noop_cm(),
+ )
+ def test_delete_role_validation_404_not_found(
+ self, mock_get_application_builder, mock_get_auth_manager, mock_roles,
test_client, as_user
+ ):
+ mgr = MagicMock()
+ mgr.is_authorized_custom_view.return_value = True
+ mock_get_auth_manager.return_value = mgr
+ mock_roles.delete_role.side_effect = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Role with name 'non_existent_role' does not exist.",
+ )
+
+ with as_user():
+ resp = test_client.delete("/fab/v1/roles/non_existent_role")
+ assert resp.status_code == 404
+
mock_roles.delete_role.assert_called_once_with(name="non_existent_role")
+
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.roles.FABAuthManagerRoles")
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.security.get_auth_manager")
+ @patch(
+
"airflow.providers.fab.auth_manager.api_fastapi.routes.roles.get_application_builder",
+ return_value=_noop_cm(),
+ )
+ def test_delete_role_validation_404_empty_name(
+ self, mock_get_application_builder, mock_get_auth_manager, mock_roles,
test_client, as_user
+ ):
+ mgr = MagicMock()
+ mgr.is_authorized_custom_view.return_value = True
+ mock_get_auth_manager.return_value = mgr
+
+ with as_user():
+ resp = test_client.delete("/fab/v1/roles/")
+ assert resp.status_code == 404
+ mock_roles.delete_role.assert_not_called()
diff --git
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py
index cc105bb05d5..c8adbc2c5b4 100644
---
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py
+++
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py
@@ -209,3 +209,23 @@ class TestRolesService:
with pytest.raises(HTTPException) as ex:
FABAuthManagerRoles.get_roles(order_by="nope", limit=10, offset=0)
assert ex.value.status_code == 400
+
+ # DELETE /roles/{name}
+
+ def test_delete_role_success(self, get_fab_auth_manager, fab_auth_manager,
security_manager):
+ security_manager.find_role.return_value = _make_role_obj("roleA", [])
+ fab_auth_manager.security_manager = security_manager
+ get_fab_auth_manager.return_value = fab_auth_manager
+
+ FABAuthManagerRoles.delete_role(name="roleA")
+
+ security_manager.delete_role.assert_called_once()
+
+ def test_delete_role_not_found(self, get_fab_auth_manager,
fab_auth_manager, security_manager):
+ security_manager.find_role.return_value = None
+ fab_auth_manager.security_manager = security_manager
+ get_fab_auth_manager.return_value = fab_auth_manager
+
+ with pytest.raises(HTTPException) as ex:
+ FABAuthManagerRoles.delete_role(name="roleA")
+ assert ex.value.status_code == 404