| 1 | # 0021 — Repository access is a role ladder, and an invitation grants nothing |
| 2 | |
| 3 | **Status:** adopted |
| 4 | |
| 5 | ## Decision |
| 6 | |
| 7 | Every repository permission check asks one question: is the actor's role at |
| 8 | least the operation's? Roles and operations share one ladder, lowest first: |
| 9 | |
| 10 | ``` |
| 11 | read < triage < write < maintain < admin < owner |
| 12 | ``` |
| 13 | |
| 14 | | Operation | Needed for | |
| 15 | |-----------|------------| |
| 16 | | `read` | clone and fetch a private repository, open issues and comment | |
| 17 | | `triage` | close, label and assign other people's issues and pull requests | |
| 18 | | `write` | push, merge, edit anyone's issues and comments, manage labels | |
| 19 | | `maintain`| repository settings: name, description, default branch | |
| 20 | | `admin` | visibility, collaborators, deleting the repository | |
| 21 | |
| 22 | The owner is `repository.ownerId`, never a row. Everyone else holds a role |
| 23 | through `repository_collaborator`, and only once `acceptedAt` is set: a |
| 24 | pending row is an invitation, and grants nothing. Declining deletes the row; |
| 25 | so does removing a collaborator, and so does a collaborator leaving. |
| 26 | |
| 27 | `RepositoryAccessService.authorize` resolves the role in the same query that |
| 28 | finds the repository and returns it as `viewerRole`, so callers that need a |
| 29 | second decision (visibility needs `admin` inside a `maintain` request) make it |
| 30 | without another query. Queries that filter many repositories at once join |
| 31 | `acceptedCollaboration(repositoryId, actor)` and decide with `canAccess`. |
| 32 | |
| 33 | ## Why |
| 34 | |
| 35 | The ladder is the one `lib/permissions.ts` already describes for |
| 36 | organizations, so organization-owned repositories can reuse it. A single |
| 37 | ordering keeps the check one comparison, and every endpoint names the lowest |
| 38 | role that may use it rather than listing roles. |
| 39 | |
| 40 | An invitation that must be accepted keeps a private repository from appearing |
| 41 | in someone's account, and their name from appearing on someone's repository, |
| 42 | without their consent. |
| 43 | |
| 44 | ## Consequences |
| 45 | |
| 46 | - Public repositories stay readable to everyone; a role only matters above |
| 47 | `read` or on private repositories. |
| 48 | - An unreadable repository still answers 404 at every rung, and a readable one |
| 49 | answers 403 when the role is too low. |
| 50 | - An invitation lapses 7 days after it was sent, computed from `createdAt`, so |
| 51 | nothing has to sweep it. A lapsed row stays, shown to admins as `expired`; |
| 52 | inviting the same user again restarts its week and sends the email again. |