Skip to content

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

Merged
duncdrum merged 1 commit into
eXist-db:developfrom
joewiz:feature/sm-permission-error-codes
Jul 4, 2026
Merged

[feature] Distinct error codes for sm: permission functions (let callers map 403 vs 400)#6464
duncdrum merged 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
@dizzzz dizzzz requested review from a team, duncdrum, line-o and reinhapa June 12, 2026 07:28
@duncdrum

Copy link
Copy Markdown
Contributor

I think there is something to be said for obfuscating the actual error in case of adversarial probing.

That being said, I don't feel strongly about it.

@line-o line-o left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR does not expose any specific errors. It just allows to distinguish between malformed and unauthorized calls.

@duncdrum

Copy link
Copy Markdown
Contributor

@line-o and graduating from one error code to the other tells me that I'm on the right track.

@line-o

line-o commented Jul 4, 2026

Copy link
Copy Markdown
Member

@duncdrum in order to raise one or the other error you do need to be able to execute XQuery code on the server

@duncdrum duncdrum merged commit d752fde into eXist-db:develop Jul 4, 2026
9 checks passed
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.

5 participants