Skip to content

feat(api): accept user and team locators on grants - #1192

Open
adlerhurst wants to merge 14 commits into
mainfrom
cursor/grant-locators-54e2
Open

adlerhurst wants to merge 14 commits into
mainfrom
cursor/grant-locators-54e2

Conversation

@adlerhurst

@adlerhurst adlerhurst commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Breaking wire update of the grants HTTP API (alpha). Create, get, and list name the bound person as user (user_id) or team (team_id) instead of principal_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) and authz.granted / authz.revoked events stay (principal_type, principal_id).

POST /grants with user.identifier is 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 is grant.invalid. user_id and team locators keep 404 / 409.

expand: ["principal"] still exists as one flag for both kinds. Extra fields are copied onto the same user / team object (no sibling principal). Missing principals stay a degraded ref. GET-by-id takes the same expand query param and the same dual user.read + team.read gate.

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 for ada.lovelace@example.com (POST /grants with { user: { user_id }, relation: "admin" }), the list shows the row at level Admin, then remove via DELETE /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' — pass
  • moon run server:generate and moon run server:check-generate — pass
  • Spanner ci / full-pr failed on TestGrantCreateLocators/unknown_identifier_is_accepted_without_a_row racing 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/server minor. Callers can create grants by user identifier or team name; create/get/list drop principal_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

  • Nested OpenAPI oneOf without a discriminator made ogen skip grant operations, so create/get/list stay a single object and XOR is enforced in the handler/service as grant.invalid.
  • Add-admin-by-email UX is out of scope (later PR).
  • Identifier create opacity is HTTP-status only: a GET of a synthetic asgn_ id still 404s. Volume/timing probes and rate limits stay on Neutral responses for grant creation by identifier #1229.
  • QueryUsers allowlist cleanup and Console people-picker replacement stay on Neutral responses for grant creation by identifier #1229 after Console drops the picker.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

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>
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nextgen Ready Ready Preview Sep 18, 2026 12:01pm UTC
nextgen-docs Ready Ready Preview Sep 18, 2026 12:01pm UTC
nextgen-mock-zitadel Ready Ready Preview Sep 18, 2026 12:01pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: cc8d93f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@zitadel/server Minor
@zitadel/cli Minor
@zitadel/testing Minor
@zitadel/server-linux-x64 Minor
@zitadel/server-linux-arm64 Minor
@zitadel/server-darwin-x64 Minor
@zitadel/server-darwin-arm64 Minor
@zitadel/server-win32-x64 Minor
@zitadel/api Minor
@zitadel/config Minor
@zitadel/components Minor
@zitadel/sdk-core Minor
@zitadel/sdk-next Minor
@zitadel/sdk-nuxt Minor
@zitadel/sdk-react Minor
@zitadel/sdk-vue Minor
@zitadel/sdk-angular Minor
@zitadel/sdk-solid Minor
@zitadel/sdk-svelte Minor
@zitadel/sdk-qwik Minor

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_id with user (user_id / identifier) or team (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=principal support with user.read + team.read gating.
  • 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.

Comment thread internal/service/grant.go
cursoragent and others added 2 commits September 9, 2026 15:28
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread internal/service/grant.go
Comment thread api/openapi/endpoints/grants/grant-user.yaml Outdated
Comment thread api/openapi/endpoints/grants/team-locator.yaml Outdated
Comment thread api/openapi/endpoints/grants/user-locator.yaml Outdated
cursoragent and others added 2 commits September 9, 2026 17:35
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>
@vercel
vercel Bot temporarily deployed to Preview – nextgen-docs September 9, 2026 22:15 Inactive
@vercel
vercel Bot temporarily deployed to Preview – nextgen-mock-zitadel September 9, 2026 22:15 Inactive
@IAM-marco
IAM-marco self-requested a review September 14, 2026 09:18
Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
@cursor

cursor Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

On enumeration: locator create already collapses miss, inactive, and ambiguous into grant.principal_not_found. loadPrincipal filters status=active; identifier lookup does the same. XOR is grant.invalid. Duplicate tuple is grant.already_exists (409). 404 does not tell a caller whether the identifier exists; 409 only means that assignment already exists, and listing grants is the same grant.* plane. We are not collapsing 409 into 404.

…-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>
@IAM-marco

Copy link
Copy Markdown
Contributor

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:

  • Zero matches returns the same success-shaped response as a created grant. The server silently does nothing and keeps the reason in the logs, like it already does for ambiguous matches.
  • grant.already_exists is also not distinguishable on this path: same success shape, nothing written twice.
  • Granting to yourself (the resolved user equals the caller) is rejected with a visible error.

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 user_id path keeps its current errors. If this PR absorbs the change, #1229 shrinks to a small allowlist cleanup.

@IAM-marco IAM-marco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

4 participants