feat(core): object-scoped permission primitives — Permission.scope object, ObjectRole, Collaborator.role (no enforcement) - #11056
Open
MichaelUray wants to merge 3 commits into
Conversation
…jectRole class, Collaborator.role First step of the unified permission model discussed in hcengineering#10966. Model primitives only - no enforcement path reads them yet: - Permission.scope accepts 'object' (core interface and TPermission) - new ObjectRole class (DOMAIN_MODEL): a named, app-declared set of object-scoped permissions for documents of a given class; reuses the core.string.Role label like TRole - optional Collaborator.role: Ref<ObjectRole>; undefined keeps today's structural collaborator semantics. Stored in the JSONB data column on Postgres, so no schema change or migration is needed. Tests: model-core builds the class into the hierarchy; postgres isDataField routes Collaborator.role to the data column. Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Declare five object-scoped Issue permissions - ReadIssue, CommentOnIssue, EditIssue, TransitionIssue, DeleteIssue - with scope 'object' and objectClass tracker.class.Issue, plus labels and descriptions in all 14 tracker locales. The declarations intentionally carry no txClass, txMatch or forbid: SpacePermissionsMiddleware treats any Permission with a matching objectClass and txClass as a restriction in restricted spaces, so these declarations cannot affect existing access decisions. They are not added to the project type's availablePermissions or to any ModulePermissionGroup, so they do not appear in the role editors either. Write-path matching and ObjectRole instances follow with enforcement. Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
…sions without txClass never match Evaluate SpacePermissionsMiddleware over a matrix of TxCreateDoc, TxUpdateDoc, TxRemoveDoc and TxMixin (non-empty attributes) for a user without a space role and a user whose role references an object-scoped permission, in restricted and unrestricted spaces. The decisions against a model with the object-scoped declarations (scope 'object', no txClass/txMatch/forbid) must equal the decisions without them, and the baseline is pinned to the current behaviour. Covers both the restricted-space fallback and the role path, including the TxMixin branch of isTxClassMatched. Negative control: the same declarations with txClass TxCreateDoc must make the matrix diverge (restricted space, user without a role: create flips from allow to deny), so the comparison demonstrably detects a behaviour-changing declaration. Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
This branch has not been deployed
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.
Context
First step of the unified permission system discussed in #10966 (sketch: #10966 (comment), proposal: #10966 (comment), agreed scope: #10966 (comment) / #10966 (comment)).
This PR adds only the four agreed model primitives. It contains declarations only: there is no enforcement, migration or UI change.
Model changes
Permission.scopeaccepts'object'(foundations/core/packages/core/src/classes.tsandTPermissioninmodels/core/src/security.ts). It marks a permission that is granted on a single document instead of through a space role.ObjectRole(DOMAIN_MODEL,core.class.ObjectRole,TObjectRoleregistered inmodels/core). It is a named set of object-scoped permissions for one class, and apps declare it. It reuses thecore.string.Rolelabel, likeTRole.Collaborator.role?: Ref<ObjectRole>. When it isundefined, a collaborator works as it does today (fields, notifications, mentions). On Postgres the field goes to the JSONBdatacolumn, becausecollaboratorSchemahas fixed columns only forattachedTo,attachedToClassandcollaborator. So there is noALTER TABLEand no migration.models/tracker/src/permissions.ts:ReadIssue,CommentOnIssue,EditIssue,TransitionIssueandDeleteIssue. Each isscope: 'object'withobjectClass: tracker.class.Issue. Labels and descriptions are added in all 14 tracker locales. The non-English strings are short machine-assisted translations, so review by native speakers is welcome.The PR creates no
ObjectRoleinstances. Below is an example of what Tracker would declare in the enforcement follow-up. It is not part of this PR:No behaviour change: why, and how it is verified
Permission.scope. Only two UI queries read it, and both exclude'object':plugins/setting-resources/src/components/spaceTypes/RoleEditor.svelte:48queries{ scope: 'workspace' }.plugins/card-resources/src/components/settings/EditRole.svelte:65queries{ scope: 'space', ... }.availablePermissionsor to anyModulePermissionGroup, so they do not appear in the role or guest editors.txClass,txMatchorforbidon purpose. In restricted spaces,SpacePermissionsMiddleware.checkPermissiontreats everyPermissionwith a matchingobjectClassandtxClassas a restriction for users without a role (fallback after the role loop). WithouttxClass,isTxClassMatchednever matches. This includes itsTxMixinbranch, which compares againstTxUpdateDoc. Write-path matching will come together with enforcement.models/core:ObjectRoleis built into the hierarchy as aDoc-derivedDOMAIN_MODELclass, andpermissionsis typedArrOf(Ref<Permission>).models/tracker: each of the five permissions is declared exactly once withscope: 'object',objectClass: Issueand notxClass/txMatch/forbid. This test fails if any of those fields is added to a declaration. NoObjectRoleinstances are created, andForbidCreateProjectis unchanged.server/middleware: a regression matrix forSpacePermissionsMiddlewarecovers:TxCreateDoc,TxUpdateDoc,TxRemoveDocandTxMixin(non-empty attributes)EditIssueThe allow/deny results with the five declarations are identical to the results without them, and the baseline is pinned to current behaviour. A negative control shows that the comparison detects a change: the same declarations with
txClass: TxCreateDocchange exactly one cell (restricted space, user with no role,TxCreateDocgoes from allow to deny). Not everytxClasswould show up in this matrix. WithTxUpdateDoc, for example, the fixture's existing update permission already decides those cases. The tracker test above is the guard against adding anytxClass/txMatch/forbid.server/postgres:isDataField(DOMAIN_COLLABORATOR, 'role') === true.rush bundle --to @hcengineering/model-all+rush show-model: the model containscore:class:ObjectRole(plus itspermissionsattribute) andtracker:permission:{Read,CommentOn,Edit,Transition,Delete}Issue. It contains noObjectRoledocuments.DOMAIN_MODELand arrive with the normal model upgrade.MODEL_VERSIONis injected at bundle time fromcommon/scripts/version.txt. Comparable additive model changes did not bump it.Design notes for the next steps
ObjectRole.permissions) applies on the write path.Collaborator.role, so there is no parallel grant document. It is an exception layer under the space scope of the groups → app → spaces model: space roles stay authoritative, and object roles grant access to individual documents.ObjectRoleis the app-declared permission set per class (point 4 of the sketch). An admin flow can list it byobjectClasswithout custom code per app.Follow-ups (separate PRs)
txMatchon the issue permissions, plus an anchor resolver for comments and attachmentsaddSecurity()for collaborators with a roleCollaboratorObjectRoleinstances for issues (Viewer / Commenter / Editor)Questions
ObjectRoleshape: should the role be bound to a class (objectClass), or carry onlypermissions? Should the issue roles (Viewer / Commenter / Editor) go into this PR after all? They are 3 model documents plus labels.Read / Comment / Edit / Transition / Deleteright?ReadIssueis used only to compose roles, because readability follows the existence of the grant. Should it still be declared?ModulePermissionGroup.permissionslater, or stay separate?txClassomitted: is it OK to keep the declarations withouttxClassuntil enforcement? The alternative is to set it now and excludescope === 'object'in the restricted-space fallback, but that would change the middleware.core.string.RoleforObjectRolefine, or do you want a dedicatedcore.string.ObjectRole? That would add 14 core locale entries.Collaboratorrecords without aroleand remains compatible either way.