Skip to content

feat: validate resourceDependencies at write time, gated by allowUserResourceDependencies - #1941

Open
serguei-gorokhov wants to merge 3 commits into
rd/1-definition-modelfrom
rd/2-write-time-validation
Open

serguei-gorokhov wants to merge 3 commits into
rd/1-definition-modelfrom
rd/2-write-time-validation

Conversation

@serguei-gorokhov

Copy link
Copy Markdown

Applicable issues

Description of changes

A new ResourceDependencyValidator enforces the two-form target language (concrete global-view path, or current-user placeholder rooted), the allowed-targets rule, the ~100-entry cap, and record shape — a pointer rule only, no permission on the target is checked or required. It runs on both application writer surfaces: ResourceController (user/public bucket PUTs) and ConfigResourceController (platform bucket), which previously bypassed application validation entirely.

The governance ceiling: user-authored apps may not declare dependencies while the new settings flag allowUserResourceDependencies is off (the default); with it on, the personal root is not declarable. The ceiling is keyed on the author (accessService.hasAdminAccess), not the destination bucket — an admin prototyping in their own bucket authors an admin app. Static settings are not hot-reloaded. README settings table documents the flag.

The story ships as three commits; the two review-hardening commits are part of this PR's intended diff:

  1. d59c25b5decoded-segment bans: the token bans (.., *, placeholder off the root) run on decoded segments, mirroring the descriptor factory's single tryDecodePath pass, so percent-encoded smuggles (%2e%2e, %2a, %63urrent-user, %75sers) are caught at validation instead of decoding into banned tokens at resolution time; legitimately encoded segments (my%20folder) remain declarable.
  2. 2feee1d0bounded work and coverage: over-cap sections inspect only the first 100 entries (no unbounded allocation, no giant 400 message); slash-only target paths are a 400 shape error instead of an ArrayIndexOutOfBounds 500; a bare type root (files, public/) is rejected as over-broad; the bulk admin-apply surface (ConfigApplyService.applyApplication) applies the same shape rules (FAILED entity result); platform-bucket shape validation runs before the underBucketLocks critical section.

Note: CustomApplicationApiTest.testApplicationListing fails on Windows on pristine development alike (a pre-existing gpt-backslash folder issue, green on CI's Linux runners) — unrelated to this change.

Behavior changes flagged for review

  • New settings flag allowUserResourceDependencies, default off — user-authored declarations are impossible until an operator turns it on; static settings, restart to change.
  • Both new validation surfaces 400/403 on declarations that previously passed untouched (no app could carry a section before, so no existing app regresses).

Checklist

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

🤖 Generated with Claude Code

Serguei Gorokhov and others added 3 commits September 2, 2026 23:11
…ResourceDependencies

A new ResourceDependencyValidator enforces the two-form target language
(concrete global-view path, or current-user placeholder rooted), the
allowed-targets rule, the ~100-entry cap, and record shape — a pointer
rule only, no permission on the target is checked or required. It runs
on both application writer surfaces: ResourceController (user/public
bucket PUTs) and ConfigResourceController (platform bucket), which
previously bypassed application validation entirely.

The governance ceiling: user-authored apps may not declare dependencies
while the new settings flag allowUserResourceDependencies is off (the
default); with it on, personal targets must be typed — a root-level
current-user/ declaration is not declarable. Static settings are not
hot-reloaded. BlobEntityValidator gains the read-side warning form of
the same rules, reusing one implementation.

Note: CustomApplicationApiTest.testApplicationListing fails on Windows
on pristine development alike (a pre-existing gpt-backslash folder issue,
green on CI's Linux runners) — unrelated to this change.

Spec: documentation repo, offline-access-delegation/implementation-specs/pr2-write-time-validation.md

Co-Authored-By: Claude Code <noreply@anthropic.com>
Security-review follow-up to the write-time validation commit: the
token bans (.., *, current-user off the root) ran on raw string
segments, while the platform canonicalizes declared paths through
ResourceDescriptorFactory's single tryDecodePath pass when building
descriptors. Percent-encoded smuggles (%2e%2e, %2a, %63urrent-user,
%75sers) therefore passed shape validation and would decode to exactly
the banned tokens at resolution time — a validate-then-normalize
mismatch inside the control this series introduces.

The validator now decodes each segment the same way the factory does
(one tryDecodePath pass, mirroring fromEntityPath) before applying the
token and root-form rules; legitimately encoded segments such as
my%20folder remain declarable.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Review-driven fixes to the validation commit:

- Bounded work: once a section exceeds the 100-entry cap only the first
  100 entries are inspected, so a huge body cannot turn validation into
  unbounded allocation and a giant 400 message.
- Slash-only target paths (zero segments after split) are a 400 shape
  error instead of an ArrayIndexOutOfBounds 500.
- A bare type root ("files", "public/") is rejected — it addresses a
  whole global view, as over-broad as the personal root the ceiling bans.
- The governance ceiling is keyed on the author
  (accessService.hasAdminAccess) rather than the destination bucket:
  an admin prototyping in their own bucket authors an admin app.
- The bulk admin-apply surface (ConfigApplyService.applyApplication)
  now applies the same shape rules, returning a FAILED entity result —
  validate identically on every write path.
- Platform-bucket PUT: shape validation runs before the underBucketLocks
  critical section — pure CPU work does not belong inside a cluster-wide lock.
- Removed the BlobEntityValidator warning backstop added in the previous
  commit: BlobEntityValidator.validate has zero main-code callers
  (pre-existing dead code), so the claimed read-side mitigation was
  unreachable. The residual surface is config-file apps only —
  admin-authored by definition, and treated as unresolvable at resolution.
- README settings table documents allowUserResourceDependencies.

Co-Authored-By: Claude Code <noreply@anthropic.com>
}
// Once over the cap the section is rejected anyway — inspect only the first MAX entries so a
// huge body cannot turn validation itself into unbounded allocation.
int inspected = Math.min(section.size(), MAX_DECLARED_DEPENDENCIES);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need to call the min function?
We already know the size is less or equal to MAX cap

return issues;
}

private static List<String> pathIssues(String at, ResourceDependency dependency) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the logic should rely on ResourceDescriptorFactory

"files": [],
"reload": 60000,
"onInvalidEntity": "abort",
"allowUserResourceDependencies": false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the setting's name says it could be applied to any resource in fact applications have it.

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.

2 participants