Authz
The authz surface is where Huudis answers two operational questions:
- "Can principal P perform action A on resource R?" —
POST /v1/authz/check. Called by every Forjio service before performing a sensitive operation. - "Can principal P assume role X, and what credentials do they get?" —
POST /v1/authz/assume-role. The STS analog.
These endpoints are the runtime evaluator. They evaluate the policies, attachments, and roles you've configured.
All endpoints accept either HMAC (signed by an access key) or session-bearer authentication — see API authentication. Most production callers are other Forjio services using HMAC; the session path exists for local-dashboard testing.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/authz/check |
Authoritative authz decision |
POST |
/v1/authz/assume-role |
Mint STS credentials for a role |
GET |
/v1/authz/whoami |
Diagnostic — returns the resolved principal |
Check authorization
POST /v1/authz/check
Evaluates the principal's effective policy set against the requested action and resource.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
principal |
object | yes | The acting principal — see below. |
action |
string | yes | The action being attempted (huudis:users:write, plugipay:payments:read). |
resource |
string | yes | The resource ARN. Wildcards are unmatched on the request side — you must pass the concrete resource. |
context |
object | no | Arbitrary additional condition keys for the evaluator. |
The principal block:
| Field | Type | Description |
|---|---|---|
type |
user | group | role | service_account |
Kind of principal. |
id |
string | The usr_… / grp_… / rol_… / svc_…. |
accountId |
string (acc_…) |
Workspace scope. Huudis enforces that the principal belongs to this workspace. |
mfaVerified |
boolean | Whether the caller's session passed MFA. Surfaces as forjio:MfaPresent in conditions. |
Response — 200 OK
{
"data": {
"decision": "Allow",
"allow": true,
"reason": "matched statement HuudisReadOnly#1 on Allow",
"matchedSid": "ReadAllAudit"
}
}
decision is the raw verdict (Allow or Deny); allow is the convenience boolean. reason is a human-readable trace useful in error messages. matchedSid echoes the Sid of the winning statement (if the policy author set one).
A Deny decision is the same 200 OK — this is an evaluation endpoint, not an enforcement one. The caller decides what to do with the answer (typically: return their own 403 upstream).
Assume role
POST /v1/authz/assume-role
Trust-policy-gated. The calling principal (HMAC) or the calling session's user must be allowed by the role's trust policy.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
roleId |
string (rol_…) |
yes | The role to assume. |
sessionName |
string (≤64) | no | Optional label that goes into the audit log. |
durationSeconds |
integer (900–43200) | no | Lifetime cap. Clamped to the role's maxSessionDurationSec. Default = role's max. |
Response — 201 Created
{
"data": {
"credentials": {
"accessKeyId": "ASIA0123456789ABCDEF",
"secretAccessKey": "BASE64_SECRET_HERE",
"sessionToken": "BASE64_TOKEN_HERE",
"expiresAt": "2026-05-13T00:30:00.000Z"
},
"role": {
"id": "rol_01KPG…",
"name": "BillingReader",
"arn": "forjio:huudis::acc_01KPG…:role/BillingReader"
},
"sessionId": "ars_01KPG…"
}
}
Use the credential triple to sign subsequent requests — see API authentication. The sessionAccessKeyId (ASIA…) is distinguishable from long-lived access keys (AKIA…) on the wire.
Errors
| Status | error.code |
When |
|---|---|---|
403 |
FORBIDDEN |
The role's trust policy denies the calling principal. Includes the trust-policy reason in error.message. |
404 |
RESOURCE_NOT_FOUND |
No such role, or role is in a different workspace. |
401 |
UNAUTHORIZED |
No HMAC and no session. |
Whoami (diagnostic)
GET /v1/authz/whoami
Echoes the resolved principal — useful for debugging credentials.
{
"data": {
"huudisSession": {
"userId": "usr_01KPG…",
"sessionId": "sess_01KPG…",
"email": "adi@forjio.com",
"activeAccountId": "acc_01KPG…"
},
"hmacPrincipal": null
}
}
When called with HMAC, hmacPrincipal is populated and huudisSession is null. When called with both, both are populated.
Policy evaluation rules
The evaluator runs in this order:
- Build the effective statement set: every policy attached to the principal, plus (for users) every policy attached to a group the user is in. Assumed-role sessions use only the role's attached policies.
- Scan for
Denystatements that match(Action, Resource, Condition). If any matches: returnDeny. - Scan for
Allowstatements that match. If any matches: returnAllow. - Default: return
Deny.
Action matching: exact string, or wildcard via trailing * (e.g. huudis:*:write matches huudis:users:write). Resource matching: exact ARN, or wildcard. Conditions: every recognised operator must evaluate true against the request context.
Condition keys
Recognised at evaluation time:
| Key | Comes from |
|---|---|
forjio:MfaPresent |
principal.mfaVerified |
forjio:CurrentTime |
Server clock |
forjio:SourceIp |
Request's req.ip |
forjio:PrincipalType |
principal.type |
huudis:WorkspaceSlug |
The active workspace's slug |
| Custom keys | The context field of the request body |
Pass any custom keys your policies reference in context. The evaluator doesn't validate the key names — mistyped keys silently never match.
Service-to-service usage
The typical caller is another Forjio service. Plugipay, before charging a card, calls:
POST /v1/authz/check
Authorization: Plugipay-Forjio-V1 keyId/...
Date: ...
{
"principal": {
"type": "user",
"id": "usr_01KPG…",
"accountId": "acc_01KPG…",
"mfaVerified": true
},
"action": "plugipay:payments:create",
"resource": "forjio:plugipay::acc_01KPG…:payment/*",
"context": {
"plugipay:Amount": 4990000
}
}
Huudis returns the decision. Plugipay enforces it. The principal in the request comes from the Plugipay session (Plugipay validated the access token; Huudis trusts the authenticated relay).
Local-dashboard usage
For testing IAM rules in the Huudis dashboard, the same endpoints accept session auth. Open the IAM → Test policies panel, enter a principal and action, and the panel POSTs to /v1/authz/check on your behalf. Useful for "if I attach this policy to Group X, what does user U gain?".
Performance
authz/check is designed to be called on every protected operation — it's hot path. Targets:
- p50 < 5ms
- p99 < 30ms
Policies are cached in-process for 60 seconds per workspace; attaching a new policy can take up to a minute to propagate. Use the cache-bust endpoint (planned) for instant invalidation in incident response.
Events
assume-role does not emit a webhook event (would be too chatty). Both endpoints write audit-log entries: iam.authz_check (with the resolved decision and matched Sid) and iam.assume_role (with the role and trust-policy verdict).
Next
- Policies — the documents being evaluated.
- Attachments — how policies bind to principals.
- Roles and Assumed sessions — STS flow.