Skip to content

Gate access administration by permission; owner-only grants stay with owners - #321

Merged
KilianBoute merged 2 commits into
mainfrom
feat/permission-gated-grants
Nov 6, 2026
Merged

KilianBoute merged 2 commits into
mainfrom
feat/permission-gated-grants

Conversation

@KilianBoute

Copy link
Copy Markdown
Member

What

Administering access is now gated by permission, not by holding what is handed out. Whoever holds members.update, projects.manageAccess, project.manageAccess, roles.* or teams.update administers that area, through a system role or a custom role.

This also fixes a bug on main: the Organisation manager could not grant Viewer (CanGrant → permissionNotHeld: markers.read), because it holds no project permissions by design.

Rules that remain

  1. Owner-only permissions (new PermissionDefinition.OwnerOnly, also served in the catalog JSON as ownerOnly). These are exactly what the legacy admin tier never had:

    • billing, storage, audit, device system tags, pages.subscription, pages.audit;
    • devices.delete, devices.managePermissions, devices.manageRecording, devices.readLogs (D1, D12);
    • workflows.create/update/delete, workflow-runs.update (D4);
    • channels.*, pages.channels (D5).

    Only owners grant, create, edit or delete a role holding one, or change the members of a team assigned such a role. Migrated admins therefore grant exactly what they could before, including through a second account. As a consequence, Project admin and Billing admin are granted by owners only.

  2. project.manageAccess alone still selects resources only inside its own scope in the project.

  3. Nobody changes their own access:

    • not a role they hold, directly or through a team (in force or not);
    • not their own team membership.

    Owners are exempt; the existing AffectsUser rule for assignments is unchanged.

API

  • Access.CanGrant / CanChange: no permission bound; owner-only check added.

  • New Access.CanManageRole(userId, teams, current, next, assignments):

    • create needs roles.create, update roles.update, delete roles.delete;
    • owner-only check and self check;
    • system and legacy roles → ErrRoleInvalid.
  • New Access.CanChangeTeamMembers(userId, team, teamRoles, members):

    • needs teams.update;
    • owner-only check and self check;
    • IdP-synced team → ErrTeamInvalid.

    Inviting someone into a team goes through this check too.

  • New Role.OwnerOnlyPermission() and GrantDenialSelf.

  • Removed GrantDenialPermissionNotHeld and GrantDecision.Project. No consumers exist yet (hub-api main has no CanGrant callers).

  • Catalog validation:

    • an owner-only permission must belong to its own group;
    • a permission that is not owner-only may not imply one that is.

Tests

  • New tests:
    • Organisation manager grants Viewer, Editor and Organisation manager, but not Project admin or Billing admin;
    • no legacy role, nor Organisation manager, Editor or Viewer, holds an owner-only permission;
    • CanManageRole and CanChangeTeamMembers cases;
    • exact owner-only list;
    • catalog validation cases.
  • Existing grant tests are updated: "not held" cases are now allowed, and owner-only cases are denied.
  • go vet ./... and go test ./... pass.

The spec (§4.5 and D11) is updated on monorepo main.

🤖 Generated with Claude Code

KilianBoute and others added 2 commits November 5, 2026 16:05
… owners

Administering access no longer requires holding what is handed out. Whoever
holds members.update, projects.manageAccess, project.manageAccess, roles.* or
teams.update administers that area, through a system or a custom role. This
fixes the Organisation manager, which could not grant Viewer because it holds
no project permissions.

Three rules remain:
- Owner-only permissions (new PermissionDefinition.OwnerOnly: billing,
  storage, audit, device system tags, device delete, permission ceilings,
  recording mode and logs, workflow authoring, channels) and the Owner role
  are granted, edited and handed out through teams by owners only, so
  delegated administrators hand out at most what the legacy admin tier holds.
- project.manageAccess alone still selects resources inside its own scope.
- Nobody changes their own access: a role they hold, directly or through a
  team, or their own team membership (owners excepted).

Adds Access.CanManageRole and Access.CanChangeTeamMembers, Role.OwnerOnlyPermission,
and GrantDenialSelf; removes GrantDenialPermissionNotHeld and GrantDecision.Project
(no consumers yet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only

Team members: team_member_exceeds_access becomes team_member_owner_only.
Role assignments and invitations add *_owner_only; *_exceeds_access now covers
resources outside a project.manageAccess caller's scope (and, for invitations,
a missing permission).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KilianBoute

Copy link
Copy Markdown
Member Author

Second commit (a210b2e), API status contracts:

  • Teams: team_member_exceeds_access → team_member_owner_only.
  • Role assignments and invitations: new role_assignment_owner_only / invitation_owner_only.
  • *_exceeds_access now covers only resources outside a project.manageAccess caller's scope. For invitations it also covers a missing permission, such as teams.update for a listed team.

No consumers use these yet: hub-api has no writers, and the hub-frontend#954 preview is mock-only.

Spec: monorepo main f0a6113 (§4.5 rewritten, D11 and the plan decision revised).

@KilianBoute
KilianBoute merged commit 4537a30 into main Nov 6, 2026
@KilianBoute
KilianBoute deleted the feat/permission-gated-grants branch November 6, 2026 08:54
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