ACL: three different string-identity rules in one authorization path #533
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Raised in review of #520 stage 3 (
3a6b2a1d). Not a live bug — filed before the enforcement seams in stages5/6 are written against the current shape.
One code path uses three comparison rules:
AclScopeRef(record-struct default equality onScopeId)PermissionTrieBuilder.Buildfilteringrow.ClusterIdOrdinalIgnoreCasePermissionTrie.CollectMatcheshashing principalsOrdinalIgnoreCaseIt works today only because
AclScopePathResolver.ResolveandPermissionTrieBuilder.Buildare handed thesame
NodeAclinstances, so theScopeIdstrings are literally equal. The moment the two sides are fedfrom different snapshots — the deploy gate reads EF entities, the node reads artifact JSON, which is exactly
the split the resolver was designed for — any case drift in a
ScopeIddrops every grant on that scope.Fail-closed and diagnosed (
UnresolvedScope), so not a security hole, but a silent authorization loss.Fix: either give
AclScopeRefan explicitOrdinalIgnoreCasecomparison, or state the ordinal contract inits doc and have the deploy gate enforce it. Note
PermissionTrieNode.Childrenis deliberatelyOrdinalandmust stay that way — it matches
RawPaths.Comparerand closed a real widening; this issue is about theid comparisons, not the path-segment ones.
Smaller item from the same review:
PermissionTrieBuildDiagnosticReason.UnresolvedScopeconflates adangling/deleted id, the deliberately-unresolvable retired
FolderSegmentkind, and a producer returning anempty list. An operator with a legacy
FolderSegmentrow gets the same message as one with a typo; aseparate
RetiredScopeKindreason would be actionable.Fixed in
06c56d77(branchfix/gitea-532-533-roles-and-string-identity).The rule, now stated once on
AclScopeRefand applied throughout: identifiers —ScopeId,ClusterId,Principal— compareOrdinalIgnoreCase; path segments compareOrdinal.Chose case-insensitive ids over enforcing the ordinal contract at the gate, for two reasons the issue didn't have. First, the gate cannot enforce it: the drift the issue describes is a serialization/snapshot difference, not something an author types. Second, there is a live source of drift that is authored —
ScopeIdis free-text in the ACL editor until the stage-7 picker lands, so an operator typingeq-…forEQ-…currently loses the grant to aUnresolvedScopethat reads exactly like a typo, which it also is.One thing the issue understated.
DraftValidator's cross-cluster check comparedownertoacl.ClusterIdwithStringComparison.Ordinal, whilePermissionTrieBuilderhas always matchedOrdinalIgnoreCase— so that was a fourth rule, and the one that fails in the expensive direction. It would reject an entire deployment as cross-cluster over a ClusterId case difference the runtime would have accepted, and the two spellings genuinely occur (see #532). The gate'sScopeIdexistence maps were ordinal too, which broke its stated invariant that "a row this rule passes is a row the resolver resolves."Where the fix went.
AclScopePathResolverre-keys its own maps in the constructor rather than documenting a comparer contract for callers. That was the point of the issue — the two producers (EF at the deploy gate, artifact JSON on the node) build those dictionaries independently, and a contract neither can check is how they diverge. Doing it inside makes the rule unbreakable from the outside. The reference scan for UNS projections is a linear compare rather than a lookup, so it needed the rule applied separately; missing it would have been the worst case — a Tag grant resolving its raw path but silently skipping a UNS path, leaving the tag writable through the NodeId the grant missed.PermissionTrieNode.ChildrenstaysOrdinal, as the issue required.Sub-item:
RetiredScopeKindsplit out ofUnresolvedScope, short-circuited ahead of the ScopeId checks so it mirrors the deploy gate's own ordering (NodeAclScopeKindRetired). Paired with a control test that a live kind with a dangling id still reportsUnresolvedScope— without it, "split the reason out" could be satisfied by relabelling every drop.Falsified by three separate reverts (ref equality → 1 test red, resolver map comparer → 6, gate comparisons → 2), plus removing the FolderSegment short-circuit → 1.