Skip to content

Group roles are decorative: admin should mean "can manage the group" #58

Description

@cuibonobo

Problem

The group vocabulary has two roles, and nothing distinguishes them:

return (group.associations ?? []).some(
  (a) =>
    a.kind === 'relationship' &&
    (a.label === 'member' || a.label === 'admin') &&
    a.recordId === entityId,
);

(packages/core/src/access.ts:70–75; the spec's group-resolution note says the same — "verify the requester is a member or admin")

So admin is vocabulary with no semantics. The sharper version of the problem: membership rosters are governed by ordinary record permissions on the _group record. Whoever has write access to that record can add or remove member/admin associations — including granting membership, which then confers whatever access group ACLs carry. Membership is self-amplifying through generic write permission, and "roster change" is indistinguishable from "renamed the group."

Decided direction

admin means "can manage the group." No role hierarchy, no custom roles — one distinction, matching what a small cohesive group (the stated scale) actually needs: the group's creator delegating roster upkeep.

  1. Role-gated group management. Mutating a _group record — updating its content, adding/removing member/admin relationship associations, deleting it — requires the requester to be an admin of that group, or the stack owner. Implemented as a named special case of the association-mutation predicate ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51 is already building in ScopedStack; _group is the system type where reference-creation is privilege-creation, so it slots into exactly that machinery.
  2. Permission entries may target a role. { access: 'group', groupId, role?: 'admin', read, write } — absent role means any member (today's behavior, fully backward compatible); role: 'admin' restricts the entry to admins. Mechanically cheap: isGroupMember already reads the labels, it just starts reporting which one matched.
  3. Bootstrap rule. The creator of a _group record is stamped as its first admin at create time, so no group is ever management-orphaned. The stack owner can always intervene regardless (owner bypasses everything).
  4. Semantics, stated in the spec: member = counted by group ACLs. admin = member + may manage the group record itself.

Per the standing decision (see #57): schema-level fallout, if any, is handled in place with no version bump — no install base yet. _group@1's content schema doesn't change here anyway; the role lives in associations and in the PermissionEntry type, which is record-level, not schema-level.

Alternative considered and set aside

Demote honestly: drop admin from the access check (only member counts), document roles as app-space decoration, and let roster protection come solely from #51's generic association gating. Defensible, but the self-amplifying-roster problem forces a _group special case in the write path regardless — at which point the machinery of the chosen option exists without its vocabulary. Rejected in discussion.

Open question (named, not resolved here)

#49 makes groups key-holding entities. Roles will eventually also answer "who may act as the group" — use its key, accept grants on its behalf, speak for it to other stacks. This issue's admin definition is deliberately about managing the group record and must not foreclose that; the acting-as question is deferred to #49 follow-on work.

Work items

  • isGroupMember → role-aware return (packages/core/src/access.ts)
  • PermissionEntry group variant gains optional role (packages/core/src/types.ts + spec §Permissions)
  • ScopedStack gating for _group mutations (content update, roster associations, delete) — coordinate with ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51's predicate
  • Create-time admin stamp for _group records
  • Tests: member cannot edit roster or group content; admin can; owner can regardless; role: 'admin' ACL entry excludes plain members; absent role behaves exactly as today; creator-is-admin bootstrap
  • Spec: §Groups states the member/admin semantics and the bootstrap rule

Refs #51 (association-mutation gating this builds on), #49 (groups as key-holding entities; acting-as deferred), #57 (no-version-bump policy while there's no install base).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions