fix(api): normalize web app permission dependency errors

This commit is contained in:
hjlarry 2026-08-12 17:03:57 +08:00
parent eaa5241b07
commit 7af391b482
6 changed files with 66 additions and 7 deletions

View File

@ -162,6 +162,7 @@ class AppWebAuthPermission(Resource):
400: "Bad Request",
401: "Unauthorized",
500: "Internal Server Error",
503: "Web App Access Service Unavailable",
}
)
@web_ns.response(200, "Success", web_ns.models[BooleanResultResponse.__name__])
@ -172,7 +173,11 @@ class AppWebAuthPermission(Resource):
raise ValueError("appId must be provided")
webapp_access = application_services().webapp_access
if not webapp_access.requires_permission_check(app_id):
try:
requires_permission_check = webapp_access.requires_permission_check(app_id)
except WebAppAccessUnavailableError:
raise WebAppAccessServiceUnavailableError() from None
if not requires_permission_check:
return dump_response(BooleanResultResponse, {"result": True})
try:
@ -187,7 +192,8 @@ class AppWebAuthPermission(Resource):
logger.exception("Unexpected error during auth verification")
raise
return dump_response(
BooleanResultResponse,
{"result": webapp_access.is_user_allowed(user_id=str(user_id), app_id=app_id)},
)
try:
is_allowed = webapp_access.is_user_allowed(user_id=str(user_id), app_id=app_id)
except WebAppAccessUnavailableError:
raise WebAppAccessServiceUnavailableError() from None
return dump_response(BooleanResultResponse, {"result": is_allowed})

View File

@ -55,6 +55,13 @@ def _get_enterprise_webapp_access_mode(app_id: str) -> WebAppAccessMode:
raise WebAppAccessUnavailableError from e
def _is_user_allowed_to_access_webapp(user_id: str, app_id: str) -> bool:
try:
return EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_id)
except (EnterpriseServiceError, httpx.RequestError, json.JSONDecodeError, UnicodeDecodeError) as e:
raise WebAppAccessUnavailableError from e
@dataclass(frozen=True, slots=True)
class ApplicationServices:
app_definitions: AppDefinitionQueryService
@ -87,7 +94,7 @@ def build_application_services(
access=WebAppAccessQueryRepository(session_factory=database_client),
webapp_auth_enabled=FeatureService.is_webapp_auth_enabled(),
access_mode_for_app=_get_enterprise_webapp_access_mode,
is_user_allowed_for_app=EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp,
is_user_allowed_for_app=_is_user_allowed_to_access_webapp,
),
explore_banner_queries=ExploreBannerQueryService(
banners=ExploreBannerQueryRepository(client=database_client),

View File

@ -847,6 +847,7 @@ Check if user has permission to access a web application.
| 400 | Bad Request | |
| 401 | Unauthorized | |
| 500 | Internal Server Error | |
| 503 | Web App Access Service Unavailable | |
### [POST] /workflows/run
**Run workflow**

View File

@ -227,6 +227,35 @@ class TestAppWebAuthPermission:
passport_service.return_value.verify.assert_called_once_with("passport")
webapp_access.is_user_allowed.assert_called_once_with(user_id=expected_user_id, app_id="app-1")
@pytest.mark.parametrize("failing_method", ["requires_permission_check", "is_user_allowed"])
@patch("controllers.web.app.application_services")
def test_maps_access_dependency_failure_to_service_unavailable(
self, application_services: MagicMock, failing_method: str, app: Flask
) -> None:
webapp_access = MagicMock()
webapp_access.requires_permission_check.return_value = True
if failing_method == "requires_permission_check":
webapp_access.requires_permission_check.side_effect = WebAppAccessUnavailableError()
else:
webapp_access.is_user_allowed.side_effect = WebAppAccessUnavailableError()
application_services.return_value = SimpleNamespace(webapp_access=webapp_access)
passport_service = MagicMock()
passport_service.return_value.verify.return_value = {"user_id": "user-1"}
with (
app.test_request_context("/webapp/permission?appId=app-1", headers={"X-App-Code": "code1"}),
patch("controllers.web.app.extract_webapp_passport", return_value="passport"),
patch("controllers.web.app.PassportService", passport_service),
pytest.raises(WebAppAccessServiceUnavailableError) as raised,
):
AppWebAuthPermission().get()
assert raised.value.data == {
"code": "web_app_access_unavailable",
"message": "Web app access service is unavailable.",
"status": 503,
}
@patch("controllers.web.app.application_services")
def test_private_app_requires_passport(self, application_services: MagicMock, app: Flask) -> None:
webapp_access = MagicMock()

View File

@ -266,9 +266,10 @@ def test_build_application_services_wires_webapp_permission(
return_value=False,
) as is_user_allowed,
):
services = build_application_services(
services = ext_application_services.build_application_services(
database_client=sqlite_session_factory,
deployment_edition=DeploymentEdition.COMMUNITY,
initialization_password="",
redis=MagicMock(spec=RedisClientWrapper),
)
requires_permission = services.webapp_access.requires_permission_check("app-1")
@ -279,3 +280,17 @@ def test_build_application_services_wires_webapp_permission(
enabled.assert_called_once_with()
get_access_mode.assert_called_once_with("app-1")
is_user_allowed.assert_called_once_with("user-1", "app-1")
def test_webapp_permission_adapter_maps_connection_failure() -> None:
failure = httpx.ConnectError("connection failed")
with (
patch(
"extensions.ext_application_services.EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp",
side_effect=failure,
),
pytest.raises(WebAppAccessUnavailableError) as raised,
):
ext_application_services._is_user_allowed_to_access_webapp("user-1", "app-1")
assert raised.value.__cause__ is failure

View File

@ -1568,6 +1568,7 @@ export type GetWebappPermissionErrors = {
400: unknown
401: unknown
500: unknown
503: unknown
}
export type GetWebappPermissionResponses = {