diff --git a/spp_api_v2_change_request/README.rst b/spp_api_v2_change_request/README.rst index 3e61291f1..1522874fc 100644 --- a/spp_api_v2_change_request/README.rst +++ b/spp_api_v2_change_request/README.rst @@ -628,6 +628,24 @@ A complete workflow from creation to application: Changelog ========= +19.0.2.0.2 +~~~~~~~~~~ + +- fix(api): an authorization failure on a change-request state + transition now returns ``403 Forbidden`` instead of ``409 Conflict``. + ``AccessError`` subclasses ``UserError`` in Odoo, so all six + state-transition endpoints — ``$submit`` / ``$approve`` / ``$reject`` + / ``$request-revision`` / ``$apply`` / ``$reset`` — which caught + ``UserError`` and returned a conflict, reported permission failures as + conflicts. A client is then told to resolve a conflict it cannot see, + and one that retries on 409 loops on a permission error that will + never clear. Reachable on ``$apply`` in particular now that applying + requires the change-request manager role, where the endpoint's own + scope check already returned 403, so the same endpoint reported two + authorization failures with different statuses. ``AccessDenied`` maps + to 403 the same way, matching the platform's global FastAPI error + handler. + 19.0.2.0.1 ~~~~~~~~~~ diff --git a/spp_api_v2_change_request/__manifest__.py b/spp_api_v2_change_request/__manifest__.py index 56a1e1cd3..e1d9bea72 100644 --- a/spp_api_v2_change_request/__manifest__.py +++ b/spp_api_v2_change_request/__manifest__.py @@ -1,7 +1,7 @@ { # pylint: disable=pointless-statement "name": "OpenSPP API V2 - Change Request", "category": "OpenSPP/Integration", - "version": "19.0.2.0.1", + "version": "19.0.2.0.2", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_api_v2_change_request/readme/HISTORY.md b/spp_api_v2_change_request/readme/HISTORY.md index 1db815178..ffadca302 100644 --- a/spp_api_v2_change_request/readme/HISTORY.md +++ b/spp_api_v2_change_request/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.0.2 + +- fix(api): an authorization failure on a change-request state transition now returns `403 Forbidden` instead of `409 Conflict`. `AccessError` subclasses `UserError` in Odoo, so all six state-transition endpoints — `$submit` / `$approve` / `$reject` / `$request-revision` / `$apply` / `$reset` — which caught `UserError` and returned a conflict, reported permission failures as conflicts. A client is then told to resolve a conflict it cannot see, and one that retries on 409 loops on a permission error that will never clear. Reachable on `$apply` in particular now that applying requires the change-request manager role, where the endpoint's own scope check already returned 403, so the same endpoint reported two authorization failures with different statuses. `AccessDenied` maps to 403 the same way, matching the platform's global FastAPI error handler. + ### 19.0.2.0.1 - fix: skip field types before getattr and isolate detail prefetch (#129) diff --git a/spp_api_v2_change_request/routers/change_request.py b/spp_api_v2_change_request/routers/change_request.py index 85a9945b3..64f02a78a 100644 --- a/spp_api_v2_change_request/routers/change_request.py +++ b/spp_api_v2_change_request/routers/change_request.py @@ -6,7 +6,7 @@ from urllib.parse import urlencode from odoo.api import Environment -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationError from odoo.addons.fastapi.dependencies import odoo_env from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client @@ -43,6 +43,25 @@ change_request_router = APIRouter(tags=["ChangeRequest"], prefix="/ChangeRequest") +def _status_for_odoo_error(exc: Exception) -> int: + """Map an Odoo exception raised by a state transition to an HTTP status. + + ``AccessError`` must be distinguished before ``UserError``: it subclasses + ``UserError`` in Odoo, so a bare ``except UserError`` reports an + authorization failure as ``409 Conflict``. That is wrong twice over -- a + client is told to resolve a conflict it cannot see, and a client that + retries on 409 (reasonable for a genuine conflict, which may clear) loops + on a permission error that never will. + + ``AccessDenied`` is grouped with ``AccessError``: both report an + authorization failure, and the platform's global handler + (``fastapi.error_handlers``) maps the pair to 403 the same way. + """ + if isinstance(exc, (AccessError, AccessDenied)): + return status.HTTP_403_FORBIDDEN + return status.HTTP_409_CONFLICT + + def _build_reference(p1: str, p2: str, p3: str) -> str: """Reconstruct CR reference from path segments (e.g., CR/2026/00001).""" return f"{p1}/{p2}/{p3}" @@ -372,7 +391,7 @@ async def submit_change_request( service.submit(cr) except UserError as e: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, + status_code=_status_for_odoo_error(e), detail=str(e), ) from e @@ -414,7 +433,7 @@ async def approve_change_request( service.approve(cr, comment=comment) except UserError as e: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, + status_code=_status_for_odoo_error(e), detail=str(e), ) from e @@ -453,9 +472,9 @@ async def reject_change_request( try: service.reject(cr, reason=action_data.reason) - except (UserError, ValidationError) as e: + except UserError as e: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, + status_code=_status_for_odoo_error(e), detail=str(e), ) from e @@ -494,9 +513,9 @@ async def request_revision_change_request( try: service.request_revision(cr, notes=action_data.notes) - except (UserError, ValidationError) as e: + except UserError as e: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, + status_code=_status_for_odoo_error(e), detail=str(e), ) from e @@ -536,7 +555,7 @@ async def apply_change_request( service.apply(cr) except UserError as e: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, + status_code=_status_for_odoo_error(e), detail=str(e), ) from e @@ -576,7 +595,7 @@ async def reset_change_request( service.reset_to_draft(cr) except UserError as e: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, + status_code=_status_for_odoo_error(e), detail=str(e), ) from e diff --git a/spp_api_v2_change_request/static/description/index.html b/spp_api_v2_change_request/static/description/index.html index 3f8fbb3fd..0c677065b 100644 --- a/spp_api_v2_change_request/static/description/index.html +++ b/spp_api_v2_change_request/static/description/index.html @@ -947,13 +947,32 @@

Changelog

+

19.0.2.0.2

+ +
+

19.0.2.0.1

-
+

19.0.2.0.0