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.

Response200 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.

Response201 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:

  1. 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.
  2. Scan for Deny statements that match (Action, Resource, Condition). If any matches: return Deny.
  3. Scan for Allow statements that match. If any matches: return Allow.
  4. 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