Skip to content

Neutral responses for grant creation by identifier #1229

Description

@IAM-marco

Part of #1022. Builds on PR #1192.

Goal

Make "grant access by email" reveal nothing about who exists in the system, and reject self-invites.

Current state

PR #1192 lets POST /grants create a grant from a user.identifier (email in the default schema; the lookup key is whatever the user schema designates via x-identifier, ADR 058). Its contract says: "Exactly one active match is required; zero or several resolve as not found", returning grant.principal_not_found. So the response tells the caller whether the address belongs to an active user. A duplicate grant returns grant.already_exists (409), which is also distinguishable.

The Console journey built on top of this shows the inviter a fixed message: "If the user exists in our system, they have been granted access to your project." That message is only honest if the API itself does not leak the difference.

Separately, internal/api/security.go:110-116 allowlists QueryUsers for sessions. Its only Console consumer is the add-admin picker that the email flow replaces, and it hands any operator a page of up to 100 users, which defeats the neutral message from the other side.

Decisions that shape this work

  • On the identifier path, zero matches returns the same success-shaped response as a created grant. The server does nothing and says nothing; reasons stay in the server logs, as PR feat(api): accept user and team locators on grants #1192 already does for ambiguity.
  • On the identifier path, an existing identical grant also returns the success shape instead of grant.already_exists. Nothing is created twice.
  • The explicit user_id path keeps its current errors; ids are not secrets the way identifiers are.
  • Granting to yourself (the resolved user equals the caller) is rejected with a visible error. Your own address tells you nothing new, so this leaks nothing.
  • Worth noting for the implementation: a neutral response does not stop volume or timing probes, so this endpoint is a rate-limiting candidate.
  • QueryUsers is removed from the session allowlist once the Console picker is gone.
  • Known limitation, on purpose: until authz: gate GrantService with grant.create after catalog has grant relations #1024 and Define System Permission Catalog & Default Bundles #420 land, any access level passes the admin check, so "only admins can create grants" is not yet enforced server-side.

Dependencies

Test first (TDD)

Write API tests first: create-by-identifier with a matching user returns success and writes the grant; with an unknown identifier it returns the same response shape and writes nothing; repeated with the same identifier it returns the same shape and writes nothing new; the caller's own identifier is rejected with a distinct error; create-by-user_id keeps today's grant.principal_not_found and grant.already_exists behavior; a session calling POST /users/query is refused after the allowlist change. Then implement.

Tasks

  • Change the identifier-path responses in the grant create flow (internal/service/grant.go) as decided above, and update the contract text in api/openapi/endpoints/grants/.
  • Add the self-grant check where the identifier resolves (compare against the session's user id).
  • Drop QueryUsers from the session allowlist in internal/api/security.go, coordinated with the Console ticket that removes the picker.

Reference

PR #1192 (user-locator.yaml, resolveUserByIdentifier); internal/domain/grant.go:19 (ErrGrantPrincipalNotFound); ADR 058 (identifier designation); ADR 054 (collaboration grants).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestgoPull requests that update go code

Type

No type

Fields

Priority

Urgent

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions