docs: hoist the deny set and document permissionsFor's limits
Two issues the Task 6 re-review raised against the fix diff. permissionsFor rebuilt the deny Set inside its loop over granted entries, making it O(grants x denies) allocations on a per-request path. Measured 632ms at 4000x4000, ~100% of it in repeated Set construction. Hoisted. permissionsFor also only half-delivers on "the obvious composition agrees with decide()". A narrow deny beneath a broad grant is not representable in a Set of strings - the set keeps post:* while decide() correctly refuses post:delete - so callers that match against the set would offer actions the server rejects. Documented the limit on the interface and pointed callers at decide()/can()/filterCan() for per-action gating. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1506,6 +1506,16 @@ export interface DecideInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthzResolver {
|
export interface AuthzResolver {
|
||||||
|
/**
|
||||||
|
* Effective permissions with denied entries removed — for coarse gating such
|
||||||
|
* as hiding a menu section.
|
||||||
|
*
|
||||||
|
* NOT authoritative. A set of strings cannot express "everything under
|
||||||
|
* `post:*` except `post:delete`", so a narrow deny beneath a broad grant is
|
||||||
|
* not representable here: the set still contains `post:*` while `decide()`
|
||||||
|
* correctly refuses `post:delete`. Gate individual actions with `decide()`
|
||||||
|
* (or `can()` / `filterCan()`), never by matching against this set.
|
||||||
|
*/
|
||||||
permissionsFor(subjectId: string, scope?: AuthzScope): Promise<Set<string>>;
|
permissionsFor(subjectId: string, scope?: AuthzScope): Promise<Set<string>>;
|
||||||
decide(input: DecideInput): Promise<AuthorizationDecision>;
|
decide(input: DecideInput): Promise<AuthorizationDecision>;
|
||||||
}
|
}
|
||||||
@@ -1572,10 +1582,13 @@ export function createAuthzResolver(options: AuthzResolverOptions): AuthzResolve
|
|||||||
const permissionsFor = async (subjectId: string, scope?: AuthzScope): Promise<Set<string>> => {
|
const permissionsFor = async (subjectId: string, scope?: AuthzScope): Promise<Set<string>> => {
|
||||||
const { assignments, granted } = await loadEffective(subjectId, scope);
|
const { assignments, granted } = await loadEffective(subjectId, scope);
|
||||||
if (!assignments.denies.length) return granted;
|
if (!assignments.denies.length) return granted;
|
||||||
|
// Hoist the deny set: rebuilding it per entry makes this O(grants x denies)
|
||||||
|
// allocations on a per-request path whose input size an operator controls.
|
||||||
|
const denySet = new Set(assignments.denies);
|
||||||
const effective = new Set<string>();
|
const effective = new Set<string>();
|
||||||
for (const entry of granted) {
|
for (const entry of granted) {
|
||||||
// A wildcard grant survives only if nothing denies it outright.
|
// A wildcard grant survives only if nothing denies it outright.
|
||||||
if (!deniedBy(assignments.denies, entry)) effective.add(entry);
|
if (!permissionMatches(denySet, entry)) effective.add(entry);
|
||||||
}
|
}
|
||||||
return effective;
|
return effective;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user