Skip to content

Admin endpoints ​

Endpoints under /api/admin/... (or /api/... for resource reads without the admin/ prefix). The realm is resolved via the Host header.

Update endpoints follow the shared write semantics: merge-patch by default (absent = unchanged, explicit null clears, [] clears a list), with the handful of full-replace endpoints called out there.

Most endpoints are cookie-only and gated through .RequiresPermission("<resource>:<action>"), and those permission strings are exactly the same as in the frontend sidebar. A few endpoints are gated by authentication only (e.g. the principal/lookup pickers) or are public (the anonymous SPA-bootstrap reads); these are called out per-row below. Routes marked Management API additionally accept an OAuth bearer token with scope modgud.management, audience urn:modgud:management-api, and the same live permission. See the Management API guide.

Users ​

Endpoint definitions in Modgud.Api/Features/Users/UsersEndpoints.cs. The resource path is singular (/api/user). Deletes are gated by user:write — there is no separate user:delete tier. There is no unlock endpoint.

MethodPathPermission
GET/api/user/lookupAuthenticated (no specific permission)
GET/api/useruser:read
GET/api/user/{id}user:read
POST/api/useruser:write
PUT/api/user/{id}user:write
DELETE/api/user/{id}user:write
DELETE/api/useruser:write (bulk delete, ids in body)
POST/api/user/{id}/restoreuser:write
POST/api/user/restoreuser:write (bulk restore, ids in body)
PUT/api/user/{id}/passworduser:write
PUT/api/user/{id}/activeuser:write
GET/api/user/{id}/groupsuser:read (direct + inherited group memberships)
GET/api/user/{id}/effective-groupsuser:read (live effective membership, incl. auto-match diagnostics)
POST/api/user/{id}/groupsuser:write (add to a group)
DELETE/api/user/{id}/groups/{groupId}user:write (remove direct membership)

Admin GDPR ​

Permanent (irreversible) PII erasure. Soft-delete goes through the regular user CRUD path above; this is the masking flow, gated on the dedicated gdpr:admin permission and requires a Reason in the body.

MethodPathPermission
DELETE/api/admin/users/{id}/permanentgdpr:admin

Admin sessions ​

MethodPathPermission
GET/api/admin/users/{id}/sessionssession:read
DELETE/api/admin/users/{id}/sessionssession:write (force logout)
MethodPathPermission
POST/api/admin/users/{id}/magic-linkuser:write

Admin 2FA grace period ​

All gated on user:write (the whole group shares the gate).

MethodPathPermission
GET/api/admin/users/{id}/security-infouser:write
PUT/api/admin/users/{id}/grace/policyuser:write
POST/api/admin/users/{id}/grace/resetuser:write
DELETE/api/admin/users/{id}/graceuser:write

Admin test accounts ​

ADR 0026. All gated on user:test-account; cookie session only (no Management API bearer token). Definitions in Modgud.Authentication/Api/Admin/AdminTestAccountEndpoints.cs.

MethodPathPermission
GET/api/admin/test-accountsuser:test-account
GET/api/admin/users/{id}/test-accountuser:test-account
PUT/api/admin/users/{id}/test-account { IsTestAccount }user:test-account
PUT/api/admin/users/{id}/test-account/fixed-email-code { Code, ExpiresAt? }user:test-account
DELETE/api/admin/users/{id}/test-account/fixed-email-codeuser:test-account

Definitions in Modgud.Authentication/Api/ExternalAuth/ProfileLinkEndpoints.cs.

MethodPathPermission
GET/api/admin/users/{id}/external-linksuser:read
DELETE/api/admin/users/{id}/external-links/{linkId}user:write (force-unlink)

Force-unlink shares the self-service unlink semantics: it hard-removes the link (freeing the (issuer, subject) slot for re-linking) and is refused if it would strip the user's only remaining authentication method.

Admin profile change requests ​

All gated on user:write (the whole group shares the gate).

MethodPathPermission
GET/api/admin/change-requestsuser:write
POST/api/admin/change-requests/{id}/approveuser:write
POST/api/admin/change-requests/{id}/rejectuser:write

Roles ​

Singular path (/api/role). Deletes are gated by permission-role:write — there is no permission-role:delete tier.

MethodPathPermission
GET/api/rolepermission-role:read
GET/api/role/{id}permission-role:read
POST/api/rolepermission-role:write
PUT/api/role/{id}permission-role:write
DELETE/api/role/{id}permission-role:write

Groups ​

Singular path (/api/group). Deletes are gated by authorization-group:write — there is no authorization-group:delete tier.

MethodPathPermission
GET/api/groupauthorization-group:read
GET/api/group/{id}authorization-group:read
GET/api/group/{id}/effective-membersauthorization-group:read
POST/api/groupauthorization-group:write
PUT/api/group/{id}authorization-group:write
DELETE/api/group/{id}authorization-group:write

Principals (polymorphic lookup API) ​

Returns active persons, groups, and service accounts mixed — used by the member picker in the frontend. The single endpoint is a lookup (/api/principal/lookup) that returns all active, non-deleted principals; it has no search query parameter and is gated by authorization-group:read (a zero-role user cannot dump the directory).

MethodPathPermission
GET/api/principal/lookupauthorization-group:read

Service accounts ​

The non-human leg of the Principal hierarchy — for client_credentials (machine-to-machine) callers. Endpoint definitions in Modgud.Api/Features/ServiceAccounts/ServiceAccountsEndpoints.cs. Deletes are gated by service-account:write — there is no service-account:delete tier; deleting a service account cascade-deletes its credentials and revokes their outstanding tokens.

MethodPathPermission
GET/api/service-accountservice-account:read
GET/api/service-account/{id}service-account:read
POST/api/service-accountservice-account:write
PUT/api/service-account/{id}service-account:write
DELETE/api/service-account/{id}service-account:write

Positions ​

Position reads are the first general Management API surface. They accept the normal Modgud admin cookie or a management bearer token; the remaining Position and terminal routes are cookie-only. Every route returns 404 while the PositionTerminals feature flag is disabled.

MethodPathPermissionAuthentication
GET/api/positionposition:readCookie or Management API bearer
GET/api/position/{id}position:readCookie or Management API bearer
POST/api/positionposition:writeCookie only
PUT/api/position/{id}position:writeCookie only
DELETE/api/position/{id}position:writeCookie only

Service account credentials ​

A "credential" is a confidential OAuth client pinned to the service account with the single client_credentials grant — the only mutation path for these SA-managed OAuth clients (/api/admin/oauth/clients rejects mutating them directly).

MethodPathPermission
GET/api/service-account/{id}/credentialsservice-account:read
POST/api/service-account/{id}/credentialsservice-account:write
PUT/api/service-account/{id}/credentials/{credId}service-account:write
POST/api/service-account/{id}/credentials/{credId}/rotateservice-account:write
DELETE/api/service-account/{id}/credentials/{credId}service-account:write

OAuth clients ​

Deletes are gated by oauth-client:write — there is no oauth-client:delete tier. Client creation is also a Management API route. Terminal intent additionally requires position:write; linking or creating a Service Account additionally requires service-account:write.

MethodPathPermissionAuthentication
GET/api/admin/oauth/clientsoauth-client:readCookie only
GET/api/admin/oauth/clients/{id}oauth-client:readCookie only
POST/api/admin/oauth/clientsoauth-client:write plus conditional resource permissionCookie or Management API bearer
PUT/api/admin/oauth/clients/{id}oauth-client:writeCookie only
DELETE/api/admin/oauth/clients/{id}oauth-client:writeCookie only
POST/api/admin/oauth/clients/{id}/regenerate-secretoauth-client:writeCookie only

OAuth scopes ​

Deletes are gated by oauth-scope:write — there is no oauth-scope:delete tier.

MethodPathPermission
GET/api/admin/oauth/scopesoauth-scope:read
GET/api/admin/oauth/scopes/{id}oauth-scope:read
POST/api/admin/oauth/scopesoauth-scope:write
PUT/api/admin/oauth/scopes/{id}oauth-scope:write
DELETE/api/admin/oauth/scopes/{id}oauth-scope:write

OAuth APIs ​

Deletes are gated by oauth-api:write — there is no oauth-api:delete tier. The create-implicit-scope convenience endpoint is gated on oauth-scope:write (the side-effect being authorised is the scope creation, not the API edit).

MethodPathPermission
GET/api/admin/oauth/apisoauth-api:read
GET/api/admin/oauth/apis/{id}oauth-api:read
POST/api/admin/oauth/apisoauth-api:write
PUT/api/admin/oauth/apis/{id}oauth-api:write
DELETE/api/admin/oauth/apis/{id}oauth-api:write
POST/api/admin/oauth/apis/{id}/create-implicit-scopeoauth-scope:write

Login providers ​

The single endpoint group for both built-in (Internal) and external (Oidc / Saml / Ldap / Kerberos) login providers. The Internal entry is auto-seeded once per realm and rejects edits / deletes — clients identify it by IsBuiltIn=true on the DTO.

MethodPathPermission
GET/api/admin/login-providers/flavorslogin-provider:read
GET/api/admin/login-providerslogin-provider:read
GET/api/admin/login-providers/{id}login-provider:read
POST/api/admin/login-providerslogin-provider:write
PUT/api/admin/login-providers/{id}login-provider:write
DELETE/api/admin/login-providers/{id}login-provider:write
POST/api/admin/login-providers/{id}/secretlogin-provider:write

Realms ​

Only available on the Control-Plane realm (the realm flagged IsControlPlane = true). Otherwise 404 — the existence of realm CRUD is hidden from tenant realms. Permissions live under the control-plane app slug (realm:read|write), not under modgud. See Realm API for the request/response shapes and the InitialAdmin requirement on POST /api/admin/realms.

Security log ​

The Security log reads only the current realm's physical database. The Control-Plane-only Platform log reads PII-free deployment events from the Global Store. Both accept category, eventType, and limit.

MethodPathPermission
GET/api/admin/auth-log?category=...&eventType=...&limit=...auth-log:read
GET/api/admin/platform-audit?category=...&eventType=...&limit=...control-plane:platform-audit:read

Neither surface has a clear/delete endpoint. Retention jobs delete only expired entries.

App info ​

The SPA bootstrap reads public realm/branding metadata anonymously. Realm-wide admin-owned config lives under /api/realm-settings; the per-Application override of that config rides inline on the App resource (see Application settings below).

MethodPathPermission
GET/api/app-infoAnonymous

/api/app-info publishes the target's resolved (App ⊕ realm) sign-in policy as SignIn: { Password, EmailCode, Passkey, MinimumLevel } so clients render only the methods the target offers (it no longer returns AuthenticationMinimumLevel; see Sign-in levels). It also publishes the resolved RegistrationFields policy (Email always Required; Username / Firstname / Lastname one of Off / Optional / Required) so clients render exactly the identity inputs the realm or App requires. See Registration Fields.

Applications ​

The per-realm list of registered apps. Endpoint definitions in Modgud.Api/Features/Admin/Apps/AppsEndpoints.cs. The system app (modgud) is seeded automatically, cannot be created through this API, and cannot be deleted — only its display name / description / permission catalog can be edited. Deletes are gated by app:write and blocked (409, with a body listing every blocker) while the App's permission catalog is still referenced by a role or resource server.

MethodPathPermission
GET/api/app/lookupAuthenticated (no specific permission)
GET/api/appapp:read
GET/api/app/{id}app:read
DELETE/api/app/{id}app:write

POST /api/app and PUT /api/app/{id} are documented below, alongside the per-Application settings override that rides on the same calls.

Application settings ​

Per-Application override of the realm defaults (ADR-0011). An App is one resource, so this override is not a separate endpoint — it rides inline on the App as a Settings field, read and written in the same call as the rest of the App (one tenant transaction).

  • Read — GET /api/app/{id} returns Settings next to the catalog. It is sparse: only the sections the App overrides are present; a null/absent section inherits the realm. (The list endpoint GET /api/app omits it.)
  • Write — POST /api/app and PUT /api/app/{id} carry Settings as the complete desired override state (a replace, not a merge): a present section sets that override, a null section clears it (→ inherit the realm), and within a section a null field inherits that field. Setting Origin.Subdomain also writes the global host→App routing map (clearing it removes the route).
  • Concurrent edits — Settings.Version (read only) is the stored version of the override; send it back as ExpectedSettingsVersion on PUT /api/app/{id}. When the override was changed meanwhile, the update is refused with 409 App.SettingsChanged and nothing is written. Without ExpectedSettingsVersion the update is not checked. The realm settings work the same way: Version on GET /api/admin/realm-settings, ExpectedVersion on PATCH, 409 RealmSettings.Changed. A write that loses a race between two servers at the database answers 409 Concurrency.Conflict.

See Applications.

MethodPathPermission
GET/api/app/{id}app:read
POST/api/appapp:write
PUT/api/app/{id}app:write

The body sections are Origin (subdomain), Branding, EmailBranding, SelfRegistration (incl. the Posture = Off / JitOnOtp / ExplicitEndpoint), RegistrationFields (per-field Username / Firstname / Lastname = Off / Optional / Required, each null = inherit), NativeGrants, Dcr, and Cimd — each nullable, each mirroring the realm-level shape minus the pieces that stay realm-only (captcha, DCR GC interval).

Invite codes ​

Single-use registration invite codes for the InviteCode self-registration posture, scoped to one App. Endpoint definitions in Modgud.Api/Features/InviteCodes/InviteCodeEndpoints.cs. Dual-auth: each mutating/reading call accepts either the admin cookie (gated on the invite-code:read/invite-code:write permission) or an M2M bearer token carrying the app-bound invite:read/invite:write OAuth scope. The mint endpoint returns the plaintext code exactly once; only its hash is stored.

MethodPathPermission
POST/api/app/{appId}/invite-codesinvite-code:write (or scope invite:write)
GET/api/app/{appId}/invite-codesinvite-code:read (or scope invite:read)
DELETE/api/app/{appId}/invite-codes/{id}invite-code:write (or scope invite:write)
GET/api/admin/invite-codesinvite-code:read (realm-wide overview across every App; admin-only)

Projection endpoints (maintenance) ​

MethodPathPermission
POST/api/admin/projections/rebuildrealm:admin
GET/api/admin/projections/consistency-checkrealm:admin

Permission checks in detail ​

PermissionEndpointFilter runs after authentication. Permission strings are two-segment <resource>:<action> inside one App's catalog — the app context is implicit from the calling endpoint group (modgud for the realm-internal admin surface, control-plane for the realm-CRUD endpoints):

1. ClaimTypes.NameIdentifier → UserId
2. The calling endpoint determines the app context (modgud or
   control-plane).
3. IPermissionService.GetUserPermissionsAsync(UserId, appSlug)
   ├── BFS over the user's groups (transitive, with visited set)
   ├── filter to groups whose BoundTo contains appSlug or "*"
   ├── filter their roles to AppId == app.Id (or IsRealmAdmin = true)
   └── bypass-pre-expand realm:admin and <r>:admin
4. PermissionEvaluator.Evaluate(grants, "<resource>:<action>"):
     has realm:admin?            → ✓
     has the exact permission?   → ✓
     has <resource>:admin?       → ✓
     otherwise                   → 403

Effective permissions are computed per request from the BFS over all the user's group memberships (transitive, including nested), expanded through the assigned PermissionRoles. See Permissions & gating for the full evaluator + UserInfo-emission story.

Pagination ​

List endpoints support:

ParamTypeMeaning
pageint1-based
pageSizeintItems per page
searchstringFull-text search
sortBystringSort field
sortDescendingboolSort direction

Response:

json
{
  "items": [ ... ],
  "totalCount": 234,
  "page": 1,
  "pageSize": 50
}

Real-time updates ​

Entities with a change stream fire a SignalR event over the UIHub after a mutation: users, service accounts, apps, positions, OAuth clients, OAuth scopes, invite codes and inbox items. The frontend (useEntityService composable) listens and refreshes those lists. Roles, groups and the other admin lists have no stream: they show another admin's change after a reload.

Hub endpoint: /signalr/ui (with auth cookie + WebSocket upgrade).

Released under the Apache-2.0 License.