feat(api-aco): code-defined folder-level permissions via FlpFactory - #5620
Open
adrians5j wants to merge 6 commits into
Open
feat(api-aco): code-defined folder-level permissions via FlpFactory#5620adrians5j wants to merge 6 commits into
adrians5j wants to merge 6 commits into
Conversation
Roles and teams can already be defined in code, via `RoleFactory` and `TeamFactory`. Folder-level permissions could not — they existed only as per-folder records assigned in the Admin UI. This adds the missing third leg. An `FlpFactory` contributes rules matched on a folder's `type` and `path`, where a trailing `/*` matches the folder plus its subtree. Matching rules are merged into FLP records on read, by decorators over `GetFlpUseCase` and `ListFlpsUseCase` — both return records carrying `path` and `type`, and both sit upstream of `getDefaultPermissions`, so `team:` targets are expanded to the current identity by the existing team resolution with no new code. Code permissions are never persisted: - `UpdateFlpUseCase` and `CreateFlpUseCase` reach `flpCrud` directly rather than through the decorated use cases, so the inheritance-sync write path structurally cannot see them. - `plugin` is absent from `AcoFolder_PermissionsInput`, so the API cannot accept it. - `Permissions.create` drops `plugin` entries — every FLP write funnels through it. - `UpdateFolder` rejects a manually set `plugin` for programmatic callers. `no-access` needed no new precedence logic: `DefaultPermissionsMerger` already gives it absolute priority when collapsing per-identity permissions, so a code rule denying access cannot be overridden from the UI. Other levels follow the existing owner > editor > viewer resolution. Registration is gated on `advancedAccessControlLayer.folderLevelPermissions` at register time, so nothing is wired up or enforced without the entitlement. In the Admin UI, code-defined permissions render locked, and the update path strips them before sending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🚓 Slop Cop ✅ Nothing worth flagging. The diff looks consistent with the PR's stated intent and the code-style rules. The PR is coherent with its stated intent: it adds code-defined folder-level permissions via FlpFactory, touching only api-aco/app-aco/shared-aco/webiny packages with additive changes and one clean rename/refactor of an existing decorator; no integrity or style issues found. Automated, non-blocking heads-up from an LLM. It can be wrong — use your judgment. Regenerates on every push. |
`target: "team:content-team"` leaked the internal wire format into the authoring
API, and the user form was worse — `admin:<id>`, where nothing in the name says
"user".
A permission now names its target directly, as `{ team: "content-team" }` or
`{ user: "<id>" }`, and `CodeFlpTarget` generates the `team:` / `admin:` string.
The type is a union with `user?: never` / `team?: never`, so specifying both, or
neither, or the old `target` form is a compile-time error. `CodeFlpTarget` also
throws on both/neither for callers coming from plain JS: silently dropping a
permission that denies access would mean accidental exposure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename `FlpProvider.getFlps()` to `getCodeFlps()` so it is clear the method returns code-defined FLPs collected from registered `FlpFactory` instances, and not folder-level permissions in general. Rename `UpdateFolderUseCaseWithoutInheritedPermissions` to `UpdateFolderUseCaseWithoutReadOnlyPermissions` — the decorator strips both inherited and code-defined permissions, so the old name only described half of what it does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The provider only resolves permissions contributed by an `FlpFactory` — FLPs stored in the database are loaded by the use cases it decorates. Rename the abstraction, its implementation and the consumer fields so the boundary is explicit, and document it on the abstraction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stored-permission filter in `CodeFlpMerger.merge` looked like cosmetic deduplication. Document that it is load-bearing: `DefaultPermissionsMerger` reduces every permission for an identity by access level, so a leftover duplicate could outrank the code-defined entry that shadows it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Authoring a code-defined FLP required hand-writing the folder type string (`FmFile`, `cms:<modelId>`), which leaks an internal encoding into user code and is easy to typo. Expose `FlpFactory.FolderType.files()` and `FlpFactory.FolderType.cmsEntries(modelId)` instead. `CodeFlp.type` stays a plain `string`, so apps that register their own folder types can keep passing those directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Roles and teams can already be defined in code, via
RoleFactoryandTeamFactory. Folder-level permissions could not — they existed only as per-folder records assigned in the Admin UI. This adds the missing third leg.Rules match on the folder's
typeandpath. The stored path form isroot/<slug>/<slug>, so the leadingrootsegment is optional and/marketing,marketing, androot/marketingare all accepted.A runnable example ships in
extensions/MyFolderPermissions.ts, next to the existingMyRole.tsandMyTeam.ts.How
Two decorators — over
GetFlpUseCaseandListFlpsUseCase. Both were chosen because they return records carryingpathandtype(the fields rules match on), and both sit upstream ofgetDefaultPermissions, soteam:targets are expanded to the current identity by the existingGetDefaultPermissionsWithTeamswith no new team-resolution code.Both are needed, not one:
ListFoldersWithFolderLevelPermissionsgoes throughlistFolderLevelPermissionsand never touchesgetFolderLevelPermissions, so decorating onlyGetFlpwould give correct single-folder checks and wrong folder listings.FlpProvidermirrorsTeamProvider— fans out over all registered factories, caches per container.Code permissions are never persisted
Four layers, the first structural:
UpdateFlpUseCaseandCreateFlpUseCasereachflpCruddirectly rather than through the decorated use cases, so the inheritance-sync write path cannot see code permissions at all — it can't write them into subtree records.pluginis deliberately absent fromAcoFolder_PermissionsInput, so the API cannot accept it.Permissions.createdropspluginentries. Every FLP write funnels through it, which matters because a client echoing back what it read would otherwise turn a code permission into a stored one nobody can remove.UpdateFolderrejects a manually setpluginwith a clear error, for programmatic callers.Precedence
no-accessneeded no new logic.DefaultPermissionsMergeralready gives it absolute priority when collapsing per-identity permissions, so a code rule denying access can't be overridden from the Admin UI and is inherited down the subtree. Other levels follow the existingowner > editor > viewerresolution, which makes code permissions a hard floor for denial and additive otherwise. Full-access users bypass code FLPs exactly as they bypass stored ones.For an identical target, the code permission wins — code entries are placed first and the stored entry for that target is dropped.
Licensing
CodeFlpsFeaturechecksadvancedAccessControlLayer.folderLevelPermissionsat register time, following the pattern from #5609. Without the entitlement nothing is registered, so a code rule is never enforced. A registeredFlpFactoryon an unlicensed project is silently inert.Admin UI
Code-defined permissions render locked, with "Defined in code and cannot be changed here." — level dropdown and remove both disabled. The update gateway and
UseCaseWithoutInheritedPermissionsstrip them before sending, so a save never trips the API guard.Testing
folder.flp.codeFlps.test.ts— merge-on-read with team expansion, folder-type isolation, subtreeno-accessdenying folder + child, exact-match not leaking to children, and read-then-write-back neither persisting nor duplicating the code permission.codeFlpPath.test.ts— path normalization and matching, including themarketingvsmarketing-archiveprefix-sibling case.packages/api-aco85 passed;app-aco93 passed;api-headless-cms-aco/api-file-manager-aco/shared-acogreen. Full monorepo build, lint, adio, and sync-dependencies all clean.One fixture note: team membership reads from the stored admin-user record via
ListUserTeamsUseCase, which these tests don't create, so identity B's team lookup is stubbed. Everything downstream — the actual code-FLP → team → identity expansion — is the real path.Not included
Docs. A
/docs/security/folder-level-permissionspage mirroring the shape of/docs/security/teamsis the natural follow-up.🤖 Generated with Claude Code