feat(api): accept user and team locators on grants - #1192
adlerhurst wants to merge 14 commits into
Conversation
Create, get, and list name the bound person as user or team instead of principal_type / principal_id. Create resolves user_id, identifier, team_id, or team name in the platform project; storage and events stay id-based. Expand inlines extras onto the same user / team ref. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: cc8d93f The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
🟡 Changes recommended
QueryGrants filtering for user_id/team_id regresses the documented contains operation by returning NotImplemented instead of performing a substring match.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR delivers a breaking (alpha) update to the Grants HTTP API by replacing (principal_type, principal_id) with explicit user / team locators, adding locator-based create semantics (user identifier / team name), and changing expand: ["principal"] to inline expanded fields onto the same user / team objects. It also updates console admin-grant creation and all relevant tests/docs/specs accordingly.
Changes:
- Replace
principal_type/principal_idwithuser(user_id/identifier) orteam(team_id/name) across create/get/list/query. - Implement locator resolution in the Grant service (user by designated identifier; team by case-insensitive name) and add GET-by-id
expand=principalsupport withuser.read+team.readgating. - Update console admin UI + tests and refresh OpenAPI + generated API artifacts + ADRs + changeset.
File summaries
| File | Description |
|---|---|
| internal/service/grant.go | Implements locator resolution, new validation, hydration toggled by expand, and updated query filtering fields. |
| internal/service/grant_test.go | Updates existing grant tests and adds service-level locator resolution coverage. |
| internal/api/grant.go | Maps new request shapes to service inputs; enforces expand gating for GET/query; maps response into new user/team shapes. |
| internal/api/grant_internal_test.go | Adds unit tests for create request mapping and updated grant response behavior. |
| internal/api/integration_test/grant_test.go | Updates integration tests for new wire format, expand behavior, and locator creates. |
| apps/console/src/routes/_authed/settings/admins.tsx | Switches admin listing/labeling and “already admin” detection to user/team objects. |
| apps/console/src/routes/_authed/settings/admins.spec.tsx | Updates MSW fixtures and assertions to the new grants wire format and expand semantics. |
| apps/console/src/components/add-admin-dialog.tsx | Updates grant creation payload to { user: { user_id }, relation }. |
| docs/adrs/README.md | Extends ADR 059 index description to mention the grant expansion exception. |
| docs/adrs/059-expanding-embedded-objects.md | Documents the grant-specific expand exception (inline extras on refs, degraded ref behavior). |
| docs/adrs/054-customer-collaboration-grants.md | Updates grant principal naming to include locator-based addressing. |
| api/openapi/endpoints/grants/user-locator.yaml | Adds UserLocator schema (user_id XOR identifier). |
| api/openapi/endpoints/grants/team-locator.yaml | Adds TeamLocator schema (team_id XOR name). |
| api/openapi/endpoints/grants/query/query-grants-request.yaml | Updates expand description to match inline-extras behavior. |
| api/openapi/endpoints/grants/query/methods.yaml | Updates query endpoint docs for inline expand semantics + gating. |
| api/openapi/endpoints/grants/query/grant-filter-field.yaml | Replaces principal filters with user_id / team_id. |
| api/openapi/endpoints/grants/query/grant-expand.yaml | Redefines principal expand as “inline extras onto user/team ref”. |
| api/openapi/endpoints/grants/methods.yaml | Updates create endpoint docs for locator inputs and ref-only 201 response. |
| api/openapi/endpoints/grants/grant.yaml | Replaces grant schema to discriminate on presence of user vs team and removes sibling principal. |
| api/openapi/endpoints/grants/grant-user.yaml | Introduces GrantUser (user-ref + inline expand extras). |
| api/openapi/endpoints/grants/grant-team.yaml | Introduces GrantTeam (team-ref + inline expand extras). |
| api/openapi/endpoints/grants/grant-expanded-principal.yaml | Removes the previous expanded-principal union schema. |
| api/openapi/endpoints/grants/create-grant-request.yaml | Updates CreateGrantRequest to accept user or team locator objects. |
| api/openapi/endpoints/grants/by_id/methods.yaml | Adds GET-by-id expand query param contract. |
| api/openapi/endpoints/grants/by_id/getGrant-error-response.yaml | Adds user.permission_denied / team.permission_denied to GET error catalog. |
| api/generated/oas_validators_gen.go | Regenerates validators for new request/response shapes. |
| api/generated/oas_unimplemented_gen.go | Regenerates unimplemented handler docs for updated grants endpoints. |
| api/generated/oas_server_gen.go | Regenerates handler interface and endpoint docs for updated grants endpoints. |
| api/generated/oas_schemas_gen.go | Regenerates Go types for new locator and grant schemas. |
| api/generated/oas_parameters_gen.go | Adds expand query decoding for GetGrant. |
| api/generated/oas_json_gen.go | Regenerates JSON encode/decode for new types and removed principal union. |
| api/generated/oas_handlers_gen.go | Regenerates server request handlers (expand param plumbing). |
| api/generated/oas_client_gen.go | Regenerates client request encoding (expand param on GetGrant). |
| .changeset/grant-api-locators.md | Adds a minor changeset describing the API wire update and locator create support. |
Review details
- Files reviewed: 26/34 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Grant expand inlines user attributes onto GrantUser, so ogen types them as GrantUserAttributes rather than UserAttributes. Compare the JSON in the expand integration test. Restore contains on user_id / team_id filters to match the previous principal_id filter. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Identifier lookup is not scoped to the schemas designating each property and can grant access to the wrong user.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 26/34 changed files
- Comments generated: 4
- Review effort level: Balanced
Identifier lookup now ANDs schema_url to the schemas that set x-identifier for that key, so a unique undesignated property on another schema cannot be selected. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
GrantUser.attributes is user content, not the schema document. Extra locator properties stay additionalProperties: false (req.invalid); XOR of id vs locator remains grant.invalid. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
configureZitadel is write-once on globalThis, which survives Vitest isolate. A file that bound the DEV `/api` default first caused later specs to fetch localhost:3000 while their MSW handlers waited on http://localhost/api, so the first test in many files timed out in CI. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
|
On enumeration: locator create already collapses miss, inactive, and ambiguous into |
…-54e2 # Conflicts: # api/generated/oas_client_gen.go # api/generated/oas_handlers_gen.go # api/generated/oas_json_gen.go # api/generated/oas_schemas_gen.go # api/generated/oas_server_gen.go # api/generated/oas_unimplemented_gen.go # api/openapi/endpoints/grants/by_id/methods.yaml # api/openapi/endpoints/grants/grant.yaml # api/openapi/endpoints/grants/methods.yaml # api/openapi/endpoints/grants/query/grant-expand.yaml # api/openapi/endpoints/grants/query/methods.yaml # internal/api/grant_internal_test.go Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Setup-file top-level _resetConfigForTesting() does not re-run for every file in a worker, so a spec that bound the DEV /api default still leaked into later MSW suites (ECONNREFUSED :3000). Reset before and after each test, and assert the write-once slot can rebind after reset. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
|
While this PR is still open, we want to change the locator-create contract. This is now tracked in #1229; raising it here because changing it pre-merge is free and post-merge it is a breaking change. What we want instead, on the identifier path only:
Why: the current contract ("zero or several resolve as not found") collapses the reasons, but it still tells the caller whether a grant was created, which answers "does this email belong to an active user". The recorded rationale for keeping 409 distinguishable was that a caller who can create grants can also list them. That covers people who already hold a grant, but create-by-identifier also answers the question for addresses that hold no grant at all, which is exactly the enumeration surface the Console flow needs closed: it shows a fixed "if the user exists in our system, they have been granted access to your project" message, and that message is only honest if the API does not leak the difference. The explicit |
POST /grants with user.identifier now returns 201 for miss, ambiguity, and duplicates so the status cannot tell whether the address exists. Self-grant from a session remains grant.invalid. user_id and team locators keep 404/409. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Give the identifier-miss create subtest its own project so a global QueryGrants length check cannot race sibling t.Parallel() creates. Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
…-54e2 # Conflicts: # api/generated/oas_json_gen.go # api/generated/oas_schemas_gen.go Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Summary
Breaking wire update of the grants HTTP API (alpha). Create, get, and list name the bound person as
user(user_id) orteam(team_id) instead ofprincipal_type/principal_id. Create accepts an id or a unique locator (user.identifier/team.name) and resolves it in the platform project. Storage (authz_assignments) andauthz.granted/authz.revokedevents stay(principal_type, principal_id).POST /grantswithuser.identifieris accepted with 201 whether or not a user matched, so the HTTP status cannot enumerate whether an address belongs to an active platform user. A miss or several matches still return a Grant and write nothing. A duplicate returns the existing row. Granting the session caller's own resolved user isgrant.invalid.user_idand team locators keep 404 / 409.expand: ["principal"]still exists as one flag for both kinds. Extra fields are copied onto the sameuser/teamobject (no siblingprincipal). Missing principals stay a degraded ref. GET-by-id takes the same expand query param and the same dualuser.read+team.readgate.Console add-admin stays picker-by-id; the request body is the mechanical
{ user: { user_id }, relation: "admin" }compile fix.Validation
Console Settings → Admins against
moon run console:dev-real(dev@zitadel.local): create a grant forada.lovelace@example.com(POST /grantswith{ user: { user_id }, relation: "admin" }), the list shows the row at level Admin, then remove viaDELETE /grants/{id}and the table returns to “No admins yet.”console_admins_create_list_delete_grant.mp4
go test ./internal/service -count=1 -run TestGrantService_Create— pass (identifier miss/ambiguous/duplicate → 201; self-grant → grant.invalid; user_id/team unchanged)go test ./internal/api -count=1 -run 'TestCreateGrant|TestGrantCaller|TestGrantResponse'— passmoon run server:generateandmoon run server:check-generate— passci / full-prfailed onTestGrantCreateLocators/unknown_identifier_is_accepted_without_a_rowracing sibling parallel creates; isolated that subtest onto its own project. Docker is unavailable here so the Spanner lane was not re-run locally.Release notes / changeset
Changeset:
.changeset/grant-api-locators.md—@zitadel/serverminor. Callers can create grants by user identifier or team name; create/get/list dropprincipal_type/principal_id. Creating by identifier always returns 201 except self-grant; it does not reveal whether the address matched.No additional changeset for the console test isolation fix — no shipped behavior changed.
Notes
oneOfwithout a discriminator made ogen skip grant operations, so create/get/list stay a single object and XOR is enforced in the handler/service asgrant.invalid.asgn_id still 404s. Volume/timing probes and rate limits stay on Neutral responses for grant creation by identifier #1229.To show artifacts inline, enable in settings.