Repository navigation
Description
Two changes to the Access Control plugin, plus a documentation pass.
Denials now say what failed. INSUFFICIENT_ENTITLEMENTS and INSUFFICIENT_SCOPES were declared in
AuthErrorCodes and never emitted — every denial reported a generic FORBIDDEN, so a caller could not
tell whether they needed different credentials or a differently-scoped token.
evaluateInboundRequirements now returns an EvaluationResult carrying the decision together with the
ErrorCode to report, instead of a bare boolean. The two come from the same evaluation pass, so the
reason cannot disagree with the decision — deriving it afterwards would mean evaluating twice, and a second
pass can reach a different conclusion, since an AccessPolicy is application code with no guarantee of
being pure.
The code is an ErrorCode, not a fixed set of framework-defined reasons, so a custom evaluator can report
something meaningful in its own domain rather than picking the nearest of a handful:
if (subscriptionExpired(context)) {
return EvaluationResult.denied(MyErrorCodes.SUBSCRIPTION_EXPIRED);
}
return EvaluationResult.allowed();The default evaluator names entitlements and scopes. Authentication, ownership and policy stay FORBIDDEN:
reporting "you are not the owner" would confirm that the resource exists and that somebody else holds it,
and a policy is application code with no code of its own to report.
Breaking: implementations of InboundAccessControlEvaluator must change their return type from
boolean to EvaluationResult. The example in the configuration documentation had gone stale and no
longer compiled; it is updated.
ReflectionUtils now caches per class. getAllFields rebuilt a map of every field in the whole class
hierarchy on every call, and it sits behind every reflective read and write the framework performs —
setFieldValueThrowing alone has 14 call sites, and each visitor writes data, links and meta back
onto the document. Access control adds one lookup per guarded field per object; sparse fieldsets adds one
per path plus one per level of its recursion. A page of a hundred resources rebuilt those maps hundreds of
times per response.
Four ClassValue caches, the same pattern already used for the access control models: fields, field types,
field paths, and shadowed field names. Class fields cannot change while the JVM runs, and ClassValue keys
weakly so nothing pins an application classloader.
Two of those methods were handing out mutable collections — fetchFieldTypes a HashMap and
getAllFieldPaths a HashSet. Harmless while every caller got a private copy, a silent corruption source
once shared. Both are now unmodifiable, with tests asserting it.
Deliberately not done: applying setAccessible while populating the cache. It would have removed the
repeated per-call flag checks, but setAccessible is currently applied lazily per field, so a class with
one inaccessible field still works for the rest. Doing it eagerly would let a single inaccessible field
fail the whole class. Behaviour is unchanged; the win is purely the eliminated allocations.
Documentation. Javadoc added across #147-#151 explained design decisions to readers who never saw
the alternatives — arguing against approaches that were never shipped. Rationale belongs in commits and
issues, not in API documentation. Ten blocks rewritten to say what the code does, what to pass, and what
will surprise you.
Activity
#152: specify custom code for 'forbidden', refactor JavaDocs
#152: add more ceachability for ReflectionUtils