Skip to content

fix(hub): grant chat staging write by role, and take it back on demotion - #222

Merged
aaron-tsar merged 3 commits into
testfrom
fix/chat-upload-role-write-test
Sep 17, 2026
Merged

aaron-tsar merged 3 commits into
testfrom
fix/chat-upload-role-write-test

Conversation

@aaron-tsar

Copy link
Copy Markdown
Contributor

Server half of the chat-attachment fix. Pairs with drumee/schemas#182 and needs that one deployed first.

Replaces #221, which targeted preview from a feature branch — this repository only accepts a PR into preview from test. Same three commits, rebased onto test.

Cause

A member whose workspace role is Chat could not attach a file in chat. The upload answered 403 and the chat showed nothing — no attachment chip, no error.

An attachment stages in the hidden folder /__chat__/__upload__ before it becomes a message. A chat member carries privilege 7 and has no write bit for the workspace, which is intended, so the invite path grants write on that one folder with assign_via 'no_traversal' to keep the raised access from reaching anything inside it.

All three call sites granted a fixed value of 4. That meant write before the permission bits were renumbered (download 2 -> 4, write 4 -> 8) and means download now, so the grant no longer satisfied the upload ACL.

Changes

_grantMembership, invite_with_roles, and set_privilege now grant write, and only to a role that may chat.

The role gate is not a precaution. The grant was previously handed out regardless of role, so view-only members already carry the row — 22 of 197 on stage. Changing the value without gating would have handed an upload path to members entitled only to read. It is written at the call site rather than a layer above, using privilegeAllows(privilege, CAN_CHAT) from service/lib/member-capability.js, whose header explains why the bit is spelled out there rather than read from Constants.permission.

set_privilege also revokes on the way down. It only ever granted, so demoting a member from chat to view-only left the staging access in place and the demoted member kept uploading. A role change has to move the grant in both directions.

The granted value is pinned, not read from the package. Privilege.WRITE resolves to 15 under server-essentials 1.3.1 and to 7 under 1.3.6, which republished the pre-1.3.0 layout. package.json asks for ^1.3.1 and only the lockfile holds 1.3.1, so a bare npm install would carry no write bit at all — the exact 403 this fixes, reintroduced by a dependency bump nobody would connect to chat.

Deploy precondition: install with npm ci. Under 1.3.6 the ACL word src: "write" becomes the chat bit and the role model is wrong well beyond chat.

Verification

Deployed to the aaron stage endpoint and exercised with real accounts, reading the permission table to confirm:

action result
invite at privilege 7 (chat) staging grant written at 15, no_traversal
invite at privilege 3 (view) no row at all
demote 7 → 3 row deleted
promote 3 → 7 row written at 15

Resolved permission afterwards: the chat member reads 15 on the staging folder and 7 on the workspace root; the view-only member reads 3 on both; the owner is unchanged at 63.

offline/test/chat-upload-grant.test.js locks the two properties an edit could quietly undo — that the gate refuses a view-only member even against a mask that overlaps read, and that the granted value carries the write bit and is not the value 1.3.6 would give. It reads the call sites as text and imports only member-capability, which requires nothing, so it runs in the existing install-free workflow. Checked against three mutations; each fails the suite.

Noted, not fixed here

permission_grant refuses to write in a workspace where no member holds 63 on '*', and five such workspaces exist on stage — their owners carry 31 or 7. Every grant there rolls back as "New granting would create orphaned hub", so invitations and role changes fail in those workspaces too, silently. Separate issue.

The three call sites that give a new member access to the hidden chat
staging folder granted a fixed value of 4. That value meant write before
the permission bits were renumbered and means download now, so a member
whose role carries no write bit could never upload an attachment and got
a bare 403 with nothing in the UI to explain it.

Grant write instead, but only to a role that may chat. The grant was
previously unconditional, so a view-only member already carried it; left
unconditional it would have handed them an upload path once the value
became meaningful.

set_privilege also has to move the grant in both directions. It only ever
granted, so demoting a member from chat to view left the staging access
behind and the demoted member kept uploading. Revoke it there instead.
… the package

Privilege.WRITE resolves to 15 under server-essentials 1.3.1 and to 7
under 1.3.6, which republished the pre-1.3.0 bit layout. package.json
asks for ^1.3.1, so a bare npm install picks up 1.3.6 and the grant would
carry no write bit at all -- the exact 403 this change exists to fix,
reintroduced by a dependency bump nobody would connect to chat.

Pin the value alongside the other capability bits, which are spelled out
for the same reason and say so in the file header.
…at value

Both properties the attachment fix rests on can be undone by an edit that
reads like a simplification, and neither shows up as a test failure today.

The gate must refuse a view-only member. The grant went out regardless of
role before, so rows for view-only members already exist in the wild;
loosening the check to a partial bit overlap would admit exactly the role
it exists to exclude, because the chat constant in the package overlaps
read.

The granted value must carry the write bit and must not be read from the
package. server-essentials 1.3.6 republished the pre-1.3.0 layout, where
Privilege.WRITE is 7 and carries no write bit in this schema, and
package.json asks for ^1.3.1 — so a dependency bump nobody connects to
chat would silently restore the 403.

Reads the call sites as text rather than loading them, and imports only
member-capability, which requires nothing. That keeps it in the same
install-free workflow as the other regressions here.

Checked against three mutations: restoring the package value, dropping
the gate from one call site, and removing the revoke on demotion. Each
fails the suite.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@aaron-tsar
aaron-tsar merged commit 7e86466 into test Sep 17, 2026
9 of 11 checks passed
@aaron-tsar
aaron-tsar deleted the fix/chat-upload-role-write-test branch September 17, 2026 10:17
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.

1 participant