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 ac496dfa556f6..9598baa96a097 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 @@ -263,6 +263,54 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - FabAuthManager + summary: Get Role + description: Get an existing role. + operationId: get_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: + $ref: '#/components/schemas/RoleResponse' + '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 e733b1bbc836a..97eb198475840 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 @@ -100,3 +100,16 @@ def delete_role(name: str = Path(..., min_length=1)) -> None: """Delete an existing role.""" with get_application_builder(): return FABAuthManagerRoles.delete_role(name=name) + + +@roles_router.get( + "/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("GET", permissions.RESOURCE_ROLE))], +) +def get_role(name: str = Path(..., min_length=1)) -> RoleResponse: + """Get an existing role.""" + with get_application_builder(): + return FABAuthManagerRoles.get_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 350a99185f80d..68d8b85d254f9 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 @@ -108,3 +108,15 @@ def delete_role(cls, name: str) -> None: detail=f"Role with name {name!r} does not exist.", ) security_manager.delete_role(existing) + + @classmethod + def get_role(cls, name: str) -> RoleResponse: + 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.", + ) + return RoleResponse.model_validate(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 97d0f1345966a..f15e715ccd554 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 @@ -332,3 +332,83 @@ def test_delete_role_validation_404_empty_name( resp = test_client.delete("/fab/v1/roles/") assert resp.status_code == 404 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_get_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_get_auth_manager.return_value = mgr + + dummy_out = RoleResponse(name="roleA", permissions=[]) + mock_roles.get_role.return_value = dummy_out + + with as_user(): + resp = test_client.get("/fab/v1/roles/roleA") + assert resp.status_code == 200 + assert resp.json() == dummy_out.model_dump(by_alias=True) + mock_roles.get_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_get_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.get("/fab/v1/roles/roleA") + assert resp.status_code == 403 + mock_roles.get_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_get_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.get_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.get("/fab/v1/roles/non_existent_role") + assert resp.status_code == 404 + mock_roles.get_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_get_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.get("/fab/v1/roles/") + assert resp.status_code == 404 + mock_roles.get_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 c8adbc2c5b4b0..1c9aada09ec4a 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 @@ -229,3 +229,26 @@ def test_delete_role_not_found(self, get_fab_auth_manager, fab_auth_manager, sec with pytest.raises(HTTPException) as ex: FABAuthManagerRoles.delete_role(name="roleA") assert ex.value.status_code == 404 + + # GET /roles/{name} + + def test_get_role_success(self, get_fab_auth_manager, fab_auth_manager, security_manager): + security_manager.find_role.return_value = _make_role_obj("roleA", [("can_read", "DAG")]) + fab_auth_manager.security_manager = security_manager + get_fab_auth_manager.return_value = fab_auth_manager + + out = FABAuthManagerRoles.get_role(name="roleA") + + assert out.name == "roleA" + assert out.permissions + assert out.permissions[0].action.name == "can_read" + assert out.permissions[0].resource.name == "DAG" + + def test_get_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.get_role(name="roleA") + assert ex.value.status_code == 404