Skip to content

fix(authority): grant a team role on the team directory, not on membership - #56

Merged
antoinetoussaint-byte merged 8 commits into
mainfrom
handoff/wiki-builder-install-20261029
Oct 30, 2026
Merged

antoinetoussaint-byte merged 8 commits into
mainfrom
handoff/wiki-builder-install-20261029

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Collaborator

Problem

The SaaS installer (codefly-dev/module-saas-starter#1273) lets a person create a team inline for each solution-declared role and grants that role to the team once the installation is active, through the existing GrantRole path. The core proved a team existed by finding a person who belonged to it, so a grant to a freshly created, empty team was refused with "<team>" is not a team of <organization>. An inline-created team has no members yet, and it has to be grantable before activation hands anyone access.

Fix

decideRoleChange asks the transaction's team directory, Tx.TeamExists(organization, id), whether the team exists in the installation's organization. Membership no longer proves a team exists. The other checks are unchanged: the organization boundary, the caller's authority, the active installation, the declared role and exactly one subject. Revocation still needs no directory entry, so a grant can still be revoked after its team is deleted.

This finishes the handoff commit b8acc41, which added Tx.TeamExists, the memory store's directory (PutTeam/DeleteTeam), the store contract case and a teams: list in the local fixture. The commit on top:

  • decide: TeamOfOrganization now documents what the directory means.
  • Local authority: the fixture is the local host's team directory. A team the fixture puts a person in exists, and teams: lists the empty ones, so existing fixtures keep working.
  • Conformance: SolutionPrincipalCase gains a required EmptyTeam field (a team of Organization that nobody belongs to). The suite grants it a role, lists the grant and revokes it. A grant to a team the host does not hold (a random UUID) is refused with not a team of.

Classification: fix, made in the core, which owns the decision. A host's team directory is the source of truth for whether a team exists.

Reused

  • The existing GrantRole / GrantRoleAsHost path and decide.ChangeRole. No new RPC, no proto or interface change, no generated code.
  • The store contract (storetest.Run) and the conformance target RunSolutionPrincipalTarget, extended the same way their existing cases are.

Tests

  • authority.TestARoleIsGrantedToATeamTheDirectoryHolds:
    • An empty team receives a grant, through both GrantRole and GrantRoleAsHost.
    • A populated team still works.
    • These are refused: a team of another organization (empty or populated), an unknown team, and a team named only by a membership.
    • A member who joins after the grant holds the role.
    • A deleted team's grant can still be revoked.
  • state.TestTheFixtureTeamDirectoryHoldsEmptyAndSeatedTeams (local fixture directory, plus refusals for malformed entries).
  • Store contract: "team directory includes empty teams and refuses foreign or absent ids" (memory store).
  • Conformance: "a team nobody belongs to yet is granted a role, listed and revoked" (local authority).
  • Both the core test and the conformance case fail with 21ce6d9's membership-based check put back, and pass with this change.
  • go vet ./... and go test -race -count=1 ./... pass in all 13 Go modules, matching the CI matrix. The hop Postgres admission regressions ran without HOP_TEST_POSTGRES.

Scope note

This branch also carries five earlier commits from fix/standalone-module-binding (940ddd6..21ce6d9: installation setup steps, signed role declarations, the declaration wire contract). Accounts already pins them at 21ce6d9, and no open PR carries them.

Adoption by accounts

Accounts must implement Tx.TeamExists on its core transaction, reading the team table by organization and id inside the transaction (the task says this exists on its branch). Then it must:

  • move authority-core/go and conformance/go to this head in module/services/accounts/code/go.mod;
  • set EmptyTeam (a seeded team with no members) in its RunSolutionPrincipalTarget case;
  • seed storetest.Identity.Teams in its store contract adapter.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 8 commits October 29, 2026 00:03
…rship

A host that creates a team inline for a solution-declared role has nobody
in that team yet, and grants the role before activation hands anyone
access. The core proved a team by finding a person who carried it, so a
grant to a freshly created, empty team was refused with "<team> is not a
team of <organization>". The core now asks the transaction's team
directory (Tx.TeamExists) whether the team exists in the installation's
organization. Every other check is unchanged: the organization boundary,
the caller's authority, the active installation, the declared role and
exactly one subject. Revocation still needs no directory entry, so a
grant stays revocable after its team is deleted.

This finishes the handoff commit b8acc41 (Tx.TeamExists, the memory
store's directory, the store contract case and the local fixture's
teams: list):

- decide: TeamOfOrganization now documents the directory meaning.
- local authority: the fixture is the local host's team directory; a team
  it seats a person in exists, and teams: lists the empty ones.
- conformance: SolutionPrincipalCase gains a required EmptyTeam (a team
  of Organization nobody belongs to), granted, listed and revoked; and a
  grant to a team the host does not hold is refused. Hosts must supply
  EmptyTeam to run the suite.

Tests:
- authority TestARoleIsGrantedToATeamTheDirectoryHolds: an empty team
  receives a grant (GrantRole and GrantRoleAsHost); a populated team still
  works; teams of another organization (empty and populated), an unknown
  team and a team only a membership names are refused; a member who joins
  after the grant holds the role; a deleted team's grant stays revocable.
- state TestTheFixtureTeamDirectoryHoldsEmptyAndSeatedTeams.
- storetest "team directory includes empty teams and refuses foreign or
  absent ids" (from b8acc41) runs against the memory store.
- conformance "a team nobody belongs to yet is granted a role, listed and
  revoked" against the local authority; it fails on 21ce6d9's core.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 67586be into main Oct 30, 2026
17 checks passed
antoinetoussaint-byte added a commit to codefly-dev/module-saas-starter that referenced this pull request Oct 30, 2026
…mmit

codefly-dev/authority#56 squash-merged as 67586be6ccb9, which orphans the
branch commits accounts pinned: authority-core and conformance at
666ea1f97f80 and authority-client at 21ce6d9284ed (both diverged from
authority main). Move all three to the merge commit so the pins resolve
from main; delivery and resources stay at f82a98e093ef, which main holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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