Access Control
Authentication mechanics, authorization checks, and error semantics across GoodMem APIs.
Access Control
GoodMem authenticates every request with an API key, then authorizes the operation the request performs. This page covers the wire mechanics and error semantics. For the model behind them — principals, grants, roles, and ceilings — start with the security model overview.
Authentication
Send the API key in one of these ways:
- CLI:
--api-key, thenGOODMEM_API_KEY, then the active CLI profile. - REST/gRPC:
x-api-key: <api-key>.
REST and gRPC accept the key only in the x-api-key header. The MCP endpoint additionally accepts
Authorization: Bearer <api-key> for compatibility with MCP clients.
A key authenticates a request when all of the following hold:
- The key has not been revoked.
- The current time falls inside the key's validity window: at or after
valid_from, and beforeexpires_atwhen one is set. - The key's subject principal is active. A key whose subject has been deleted stops authenticating, even though the key itself is never marked revoked.
A request that fails any of these returns UNAUTHENTICATED over gRPC and 401 Unauthorized over
REST. The response does not say which condition failed.
Endpoints That Skip API-Key Authentication
Three methods run without an API key:
| REST route | gRPC method | Reason |
|---|---|---|
POST /v1/user-enrollments:complete | UserService/CompleteUserEnrollment | The caller has no API key yet. The one-time enrollment credential authenticates the call, and the endpoint is rate-limited (see below). |
POST /v1/system/init | UserService/InitializeSystem | One-time instance bootstrap, before any key exists. |
GET /v1/system/info | SystemService/GetServerInfo | Server metadata clients may need before authenticating. |
Every other GoodMem API method requires a key. Infrastructure endpoints are also served without a
key: gRPC health checking and reflection, the REST probes /health, /livez, /readyz, and
/startupz, and the /openapi documentation UI.
Authorization
Each API method maps to an operation from the operations catalog, applied to a target resource. The request succeeds when the authenticated principal's live authority — assembled from direct grants, role assignments, all-authenticated grants, and ownership — allows that operation on that target. When the request authenticates through a scoped API key, the key's immutable permission ceiling must allow the same operation and target independently.
If authentication succeeds but authorization fails, gRPC returns PERMISSION_DENIED and REST
returns 403 Forbidden.
Error Precedence
Most resource API requests are validated in this order, and the first failure determines the status:
INVALID_ARGUMENT— the request is malformed.NOT_FOUND— the target does not exist in the requested lifecycle view. Soft-deleted principals returnNOT_FOUNDunless the request setsinclude_deleted.PERMISSION_DENIED— the target exists, but the caller lacks authority over it.FAILED_PRECONDITION— the target's state does not permit the transition, such as reactivating a revoked API key or updating a deleted principal.ALREADY_EXISTS— a uniqueness conflict, such as reusing an email, username, or client-provided UUID.
Lookups by email or username are an exception to step 3: a missing record and an existing but
unreadable record both return NOT_FOUND, so these guessable identifiers do not reveal whether a
user exists. UUID lookups keep the ordinary NOT_FOUND/PERMISSION_DENIED distinction.
Some RPCs enforce caller-type preconditions before resolving the target. For example, only human
principals may administer user enrollments. CompleteUserEnrollment also deliberately collapses
unusable enrollment credentials to UNAUTHENTICATED rather than exposing target or authorization
details.
HTTP Status Mapping
REST endpoints translate gRPC status codes as follows:
| gRPC status | HTTP status | Notes |
|---|---|---|
UNAUTHENTICATED | 401 | Missing, invalid, expired, or revoked key, or a deleted subject. |
PERMISSION_DENIED | 403 | Authenticated but not authorized. |
NOT_FOUND | 404 | Includes unreadable email/username lookups, as above. |
FAILED_PRECONDITION | 412 | 409 instead when the server marks the failure as a resource-in-use conflict. |
ALREADY_EXISTS | 409 | Uniqueness conflict. |
ABORTED | 409 | Transient database conflict (serialization failure or deadlock). Safe to retry with backoff. |
RESOURCE_EXHAUSTED | 429 | Rate limited. The response carries a Retry-After header. |
Enrollment Completion Rate Limits
POST /v1/user-enrollments:complete is unauthenticated, so it carries its own throttle,
independent of license-based request quotas. The limits apply even when the license sets no API
limits at all.
Default policy:
- 20 attempts per minute per client source address.
- 200 attempts per minute in a shared fallback bucket, used when no peer address is available.
- Each bucket permits a burst up to its full capacity and refills continuously at the stated per-minute rate.
Rejected attempts return RESOURCE_EXHAUSTED over gRPC and 429 over REST, with a Retry-After
header.
OCR
OCR requests require the OCR_DOCUMENT operation. Missing or invalid credentials yield
UNAUTHENTICATED/401, and missing authority yields PERMISSION_DENIED/403. OCR is provided by
the GoodMem OCR add-on service/image and is not included in the base install, so requests fail if
the add-on is not enabled even when authority is present.
See also: