Skip to content

Projects API for a person's token or the service account (PUT/GET /api/projects/:id) - #5238

Closed
stuartc wants to merge 31 commits into
mainfrom
stuart/con-329-projects
Closed

stuartc wants to merge 31 commits into
mainfrom
stuart/con-329-projects

Conversation

@stuartc

@stuartc stuartc commented Oct 9, 2026

Copy link
Copy Markdown
Member

Description

Adds PUT /api/projects/:id, which creates a project under a client-supplied id or replaces its name and description, and opens GET /api/projects/:id to the service account. It's the first resource beyond users that jig's apply can write, and it builds the groundwork every later resource opts into: one pipeline that accepts either a person's bearer token or the service account's access token, and a service-account case in Projects.Scope and Policies.ProjectUsers. The API contract is jig's ADR 0024 (Projects section).

How the pieces fit:

  • LightningWeb.Plugs.PersonOrServiceAccountAuth sits in a per-resource pipeline that names its scope (:projects_api names projects:write). A service-account token goes to AccessTokenAuth and is held to that scope; any other token goes through UserAuth exactly as before, and the policy decides. Only GET/PUT /api/projects/:id use it; index, the nested project routes and provisioning are unchanged.
  • The service account's %Scope{} holds no role and no membership. ProjectUsers decides for it in one clause from a written-out, deny-by-default list of actions (every project action except publish_template and the per-member alert settings).
  • Creating a project is Provisioning's :create_project: a superuser for a person, the service account behind projects:write.
  • create_project and update_project now take an actor (Lightning.Actor.t(), a user or the service account) and audit created, each initial collaborator_added, and name and description changes. notify replaces the old schedule_email? flag.
  • Members are given by email and resolved in one query through Projects.members_by_email/1, which CON-330's members route can reuse.

Two answers change for existing callers:

  • A service-account token request that names no scope is now refused with 400 invalid_scope instead of being granted every scope.
  • GET /api/projects/:id answers 403 when the policy refuses (it used to be 401, which reads as a bad token), and 409 scheduled_for_deletion to the service account.

Closes #

Validation steps

  1. mix test test/lightning_web/controllers/api/project_controller_test.exs test/lightning_web/plugs/person_or_service_account_auth_test.exs test/lightning/policies/project_user_permissions_test.exs test/lightning/projects/scope_test.exs
  2. With SERVICE_ACCOUNT_PUBLIC_KEY set, exchange an assertion for a token with scope=projects:write, then PUT /api/projects/<uuid> with {"name": "acme", "description": "…", "members": [{"email": "…", "role": "owner"}], "notify": false}. Expect 201 with the members; send it again and expect 200 with the members unchanged.
  3. Check the audit trail on the project's history: created and one collaborator_added per member, with the service account as actor.
  4. jig's scripts/e2e.sh against this branch runs the whole thing end to end.

Additional notes for the reviewer

  1. The scope is declared on the router pipeline rather than in the controller or policy. We looked at how other Phoenix apps do it (Plausible declares a scope per route, Pleroma per controller action) and kept it at the edge: a token's scope is a property of the credential, roles stay in the policy, and the plug refuses to compile without a scope:.
  2. ProjectUsers.authorize/3 now accepts any Scope.actor(), so a repo connection on GET /api/projects/:id is refused instead of raising.
  3. update_project/3 requires an actor now, and update_project_with_users/4 audits name and description changes too, so sandbox renames and admin form edits leave a trail.
  4. Projects created through /api/provision (project import) still write no created event. That path is untouched here.
  5. The branch also carries 9e70e80539 (drops .claude/commands/commit.md and turns Claude attribution off in .claude/settings.json), which isn't on main yet.
  6. No CHANGELOG entry yet; that comes once the stack settles.
  7. Two simultaneous creates of one id answer the loser with a 200 replace. That path is covered by reading and by a context test on the changeset error, not by an HTTP race test.

AI Usage

Please disclose whether you've used AI anywhere in this PR (it's cool, we just
want to know!):

  • I have used Claude Code
  • I have used another model
  • I have not used AI

You can read more details in our
Responsible AI Policy

Pre-submission checklist

  • I have performed an AI review of my code (we recommend using /review
    with Claude Code)
  • I have implemented and tested all related authorization policies.
    (e.g., :owner, :admin, :editor, :viewer)
  • I have updated the changelog.
  • I have ticked a box in "AI usage" in this PR

stuartc added 30 commits October 9, 2026 12:16
Claude Code's own commit habits cover what the command said. The one rule it held that nothing else did, no Claude attribution on commits or PRs, now lives in a checked-in .claude/settings.json so the harness enforces it.
…-owner index

Children of a has_many cast are written in list order, and membership_params/2 lists existing members in whatever order the unordered preload returns them. When the new owner's row came first, its promotion hit the non-deferrable one-owner index before the old owner's demotion. project_with_users_changeset/2 now moves the row that ends up as owner to the end of the list, so every membership write path gets the same order.
@stuartc

stuartc commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Folded into #5239, which now goes straight to main with the projects work as its own commit.

@stuartc stuartc closed this Oct 12, 2026
@stuartc
stuartc removed this pull request from stack #5240 October 12, 2026 14:35
@stuartc
stuartc deleted the stuart/con-329-projects branch October 12, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant