Skip to content

feat: add patches with per-patch roles, app links and a patches claim - #288

Merged
jaspermayone merged 9 commits into
mainfrom
feature-patches
Nov 12, 2026
Merged

jaspermayone merged 9 commits into
mainfrom
feature-patches

Conversation

@jaspermayone

@jaspermayone jaspermayone commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Weave now models patches. Each patch has its own roles and can have linked OAuth apps. Apps read a person's patch roles from a new patches claim. Quilt uses the claim to let a patch lead read the pool and their own patches only (patchworklabsorg/quilt, branch feature-patch-access).

  • Patches: a new patches table (slug, name, description), with admin pages at /admin/patches. The database refuses a slug change (patches_slug_fixed trigger) and a bad slug (patches_slug_format). The migration creates a patch for each program key in config/kyc/knot.yml (ganymede today). bin/rails patches:sync and the seeds do the same, and are safe to run again. KnotSettings now refuses a program key that is not a valid patch slug.

  • Patch roles, defined per patch: like app roles, each patch has its own role keys (PatchRole), given to a person or to a manual group (PatchRoleAssignment). patch_lead stays the governance role (locked rules unchanged), and shows as lead. The key lead is reserved.

  • Who changes what: only a superadmin creates, renames or deletes a role, and gives one. Any admin removes an assignment, so access is quick to take away. A system group (everyone, staff, ...) cannot hold a patch role. When a group holds a patch role, only a superadmin adds members, by every path (group managers, access requests, admin panel, rake tasks).

  • The patches claim: a new user scope patches. The claim maps each patch slug to the sorted keys the person holds there, for example {"ganymede": ["lead", "quilt:viewer", "reviewer"]}. It is {} when the person has none. Only an app whose scopes field lists patches gets it. Every app of a patch gets the same claim, so a role meant for one app takes that app's prefix (quilt:viewer). The directory API sends the same object to apps with the scope.

  • App links: an OAuth app can belong to one patch. Only a superadmin sets the link. Introspection of the app's client_credentials tokens adds "patch": "<slug>".

  • Moving app roles: bin/rails patches:import_app_roles APP=<app> PATCH=<slug> PREFIX=<prefix> ACTOR=<superadmin email> [APPLY=1] copies an app's roles and assignments into patch roles. Dry run by default. It refuses an app that is not linked to the patch, reports conflict, previously removed and invalid items, and never touches the app roles.

  • Directory filter: GET /api/v1/directory/users?patch=<slug>&patch_role=<key> (patch_role=lead for leads) lists holders, direct or through an active group membership. Only for an app that lists patches and is linked to that patch (403 without the scope, 404 for another patch or an unknown key).

flowchart LR
  P[(patches)] --> UR[user_roles: patch_lead, scope = slug]
  P --> R[patch_roles: quilt:viewer, reviewer ...]
  R --> RA[patch_role_assignments: user or manual group]
  UR --> PR[PatchRoles]
  RA --> PR
  PR --> C["patches claim: {ganymede: [lead, quilt:viewer]}"]
  C --> Q[Quilt: pool + own patches, read only]
  A[OAuth app] -- patch_id --> P
  A --> I["introspection: patch = slug"]
Loading

Rollout order

Each step is safe to stop after.

  1. Merge this PR. Staging deploys from main. Then deploy production. Migrations only add tables and a column, and create ganymede. Apps that do not ask for patches see no change.
  2. Configure Weave (superadmin), staging then production: link the Krater app to the ganymede patch. Add patches to the scopes of the Quilt and Krater apps. Check that krater-reviewers and krater-admins are manual groups. If the Quilt app is restricted, give the leads access grants. Check that Adam holds patch_lead for ganymede.
  3. Run the import, staging then production: bin/rails patches:import_app_roles APP=<krater> PATCH=ganymede PREFIX=krater ACTOR=<superadmin email>, read the plan, then the same with APPLY=1. After this, only a superadmin adds members to a group that holds a patch role: tell the group managers first.
  4. Deploy Krater (feat: read krater roles from the ganymede patch roles in weave's patches claim krater#66). Only after step 2, or Krater sign-in fails with invalid_scope.
  5. Deploy Quilt (patchworklabsorg/quilt#63). Only after step 2, or every Quilt sign-in fails. Steps 4 and 5 can go in either order. Then check that Adam lands on Ganymede and sees Funds.
  6. Docs: docs: add the patches scope to quilt's weave app infra#98 at any time. patchworklabsorg/internal-docs#11 after steps 4 and 5, then bun run sync:docs in internal.
  7. Later: add quilt:viewer roles where needed. A Krater PR drops the app roles from its mapping. Then remove Krater's app roles in Weave.

Rollback: Weave migrations only add, so redeploy the old image and keep the tables. Krater and Quilt go back to their previous images; the extra scope on their Weave apps is harmless. The import never changes app roles: to undo it, remove the patch role assignments.

Checks

  • bundle exec rspec: 2864 examples, 0 failures.
  • bin/rubocop: no offenses. bin/brakeman: 0 warnings. zeitwerk:check: all good.
  • Two adversarial reviews ran. Their confirmed findings are fixed on this branch (owner/superadmin gates, system groups, group managers, claim only for apps that list patches, key prefix).

Not done

  • No UI screenshots (1280 and 390 wide, light and dark), as docs/UI.md asks.
  • The command bar's live search does not find patches.
  • active_directory:add_ou_to_group adds members with no added_by, so it now fails for a group that holds a patch role. That is on purpose.

Related: patchworklabsorg/quilt#63, patchworklabsorg/krater#66, patchworklabsorg/internal-docs#11, patchworklabsorg/infra#98.

@jaspermayone
jaspermayone requested a review from a team November 12, 2026 16:39
@jaspermayone jaspermayone added the migration Includes database migrations label Nov 12, 2026
…role import, and add a patch role directory filter
@jaspermayone
jaspermayone merged commit 63ce83c into main Nov 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

migration Includes database migrations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant