Skip to content

[feature] Distinct error codes for sm: permission functions (let callers map 403 vs 400)#6464

Open
joewiz wants to merge 1 commit into
eXist-db:developfrom
joewiz:feature/sm-permission-error-codes
Open

[feature] Distinct error codes for sm: permission functions (let callers map 403 vs 400)#6464
joewiz wants to merge 1 commit into
eXist-db:developfrom
joewiz:feature/sm-permission-error-codes

Conversation

@joewiz

@joewiz joewiz commented Jun 11, 2026

Copy link
Copy Markdown
Member

[This PR was co-authored with Claude Code. -Joe]

Problem

The sm: permission functions (sm:chmod / sm:chown / sm:chgrp, and the mode validators behind sm:has-access / sm:mode-to-octal) throw the same generic ErrorCodes.ERROR for unrelated failures — permission-denied, a malformed mode string, and a mode syntax error are distinguishable only by message text:

  • permission denied — catch (PermissionDeniedException) { throw new XPathException(this, e); }
  • bad mode string — throw new XPathException(this, "Mode string must be partial …")
  • mode syntax error — catch (SyntaxException se) { throw new XPathException(this, se.getMessage(), se); }

XPathException's default error code is ErrorCodes.ERROR, and none of these sites override it (the throwable-carrying constructor only adopts a code when the cause is an XPathErrorProvider, which PermissionDeniedException is not). So all three surface as ERROR.

An HTTP API layer wants to map a permission failure to 403 Forbidden and an invalid argument to 400 Bad Request, but with one generic code the only discriminator is the message string — brittle (wording can change; no contract). existdb-openapi currently works around it by classifying per operation (a chmod/chown/chgrp failure ⇒ 403, a set-mime-type failure ⇒ 400) rather than inspecting the error. (Surfaced by the existdb-openapi/db-core work.)

Change

Assign two distinct, stable codes — following the documented eXist EXXQDY scheme — so callers can branch on $err:code:

code meaning thrown for
EXXQDY0007 Permission denied PermissionDeniedException (chmod/chown/chgrp/ACL, get-permissions)
EXXQDY0008 Invalid argument malformed mode string / mode syntax error

The TransactionException \| PermissionDeniedException multi-catch in the main dispatch is split, so a transaction failure keeps the generic code (it's a server error → 500, not a 403).

Now an API layer maps cleanly:

try { sm:chmod($uri, $mode) }
catch err:EXXQDY0007 { (: 403 :) }
catch err:EXXQDY0008 { (: 400 :) }

and existdb-openapi's db-core:set-permissions can drop its per-operation heuristic.

Files

  • ErrorCodes.java — add EXXQDY0007 / EXXQDY0008 in a labeled Security/permission block.
  • PermissionsFunction.java — thread the codes through the four throw sites; split the multi-catch.
  • permission-error-codes.xqm (new XQSuite test, auto-discovered by SecurityManagerTests) — asserts both codes fire: invalid-argument via sm:has-access (overlong mode) and sm:mode-to-octal (bad syntax); permission-denied via a guest sm:chmod of an admin-only rwx------ resource.

Test

SecurityManagerTests7/7 green. exist-core builds clean; Codacy PMD clean on both changed Java files.

Scope note

This covers the PermissionsFunction permission/mode failures — the ones an HTTP layer most needs to classify. The same generic-code pattern (new XPathException(this, "message") with no code) is pervasive across the rest of the sm: module (AccountManagement, AccountStatus, FindGroup, …); extending distinct codes there is a natural follow-up. If the team would prefer a dedicated security code family over reusing EXXQDY, that's an easy rename before merge.

sm:chmod / sm:chown / sm:chgrp (and the mode validators) threw the generic
ErrorCodes.ERROR for every failure cause -- permission-denied, a malformed
mode string, and a mode syntax error were indistinguishable except by
message text. An API layer (existdb-openapi, eXide) that wants to map a
permission failure to HTTP 403 and an invalid argument to 400 had no stable
discriminator to branch on, and resorted to per-operation heuristics.

Assign two distinct, stable codes so callers can branch on $err:code:
  - EXXQDY0007 "Permission denied"  (PermissionDeniedException)
  - EXXQDY0008 "Invalid argument"   (bad mode string / mode syntax error)

The TransactionException | PermissionDeniedException multi-catch is split so
a transaction failure keeps the generic code (a server error, not a 403).

Adds permission-error-codes.xqm asserting both codes fire: invalid-argument
via sm:has-access (overlong mode) and sm:mode-to-octal (bad syntax);
permission-denied via a guest chmod of an admin-only rwx------ resource.
SecurityManagerTests: 7/7 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joewiz joewiz requested a review from a team as a code owner June 11, 2026 00:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant