Skip to main content

IAM

The IAM (Identity and Access Management) module provides authentication, identity management, and fine-grained authorization for the SOAT platform. It implements an AWS IAM-inspired policy engine with structured policy statements supporting Effect, Action, Resource, and Condition.

Overview

SOAT uses a policy-based access control model. Every API request is authenticated via JWT (for users) or an API key. Authorization is evaluated entirely through the attached policy documents — there is no separate project membership gate.

The IAM module covers:

  • Users — identity management, roles, and JWT authentication (see Users below)
  • Policy Documents — structured permission rules attached to users and API keys (see Policies)
  • Policy Engine — evaluation logic that resolves allow/deny decisions at request time
  • Authorization Model — how policies are resolved for each caller type (see Authorization Model below)

Authentication

SOAT supports two authentication methods. Both use the Authorization: Bearer <token> header.

JWT (Users)

Users authenticate via POST /api/v1/users/login with username and password. The server returns a signed JWT containing the user's public ID and role. Admin users bypass policy evaluation and have unrestricted access. Regular users are authorized through the policies attached to their account.

API Keys

API keys are prefixed with sk_ and identified by a key_-prefixed public ID. They are always scoped to a single project via project_id and may optionally have their own policy list. When an API key has policies attached, authorization applies intersection semantics: both the owning user's policies and the key's own policies must independently allow the action. This ensures API keys can never exceed the permissions of the user who created them. See API Keys for details, or watch intersection semantics block an escalation attempt in Permissions in Practice - Step 7 (Verify permissions).

Policy Documents

A policy document is a JSON object containing one or more statements. Each statement describes a permission rule.

{
"statement": [
{
"effect": "Allow",
"action": ["documents:GetDocument", "documents:ListDocuments"],
"resource": ["soat:proj_ABC:document:doc_XYZ"]
},
{
"effect": "Deny",
"action": ["secrets:*"],
"resource": ["soat:proj_ABC:secret:sec_PROD_KEY"]
}
]
}

Statement

FieldTypeRequiredDescription
effectstringYes"Allow" or "Deny"
actionstring[]YesActions this statement applies to (supports wildcards)
resourcestring[]NoSRNs this statement applies to (default: ["*"])
conditionobjectNoConditions that must be true for the statement to apply

Policy documents are created and managed globally via the Policies module and attached to users or API keys. For a worked example building both a full-access and a read-only document, see Permissions in Practice - Step 4 (Create policies).

SOAT Resource Names (SRNs)

Every addressable entity has a canonical identifier called a SOAT Resource Name:

soat:<project_id>:<resource_type>:<resource_id>

Examples:

SRNDescription
soat:proj_ABC:document:doc_XYZA specific document
soat:proj_ABC:document:*All documents in a project
soat:proj_ABC:file:*All files in a project
soat:proj_ABC:actor:actor_123A specific actor
soat:*:*:*Everything (admin-level)

Project Segment and Policy Scoping

Because policies are global (not scoped to any project), the <project_id> segment in an SRN is the primary mechanism for restricting access to specific projects.

In practice:

  • resource: ["*"] — matches all resources in all projects. Use only for broad access.
  • resource: ["soat:proj_ABC:*:*"] — restricts access to resources in proj_ABC only.
  • resource: ["soat:*:document:*"] — matches all documents across all projects.
tip

To give a user (JWT) access to a specific project, create a policy with resource: ["soat:proj_ABC:*:*"]. This achieves project-level scoping entirely through the policy engine. API keys are always scoped to a single project via project_id (see API Keys).

Resource Types

Resource TypePublic ID PrefixModule
documentdoc_Documents
filefile_Files
actoractor_Actors
conversationconv_Conversations
projectproj_Projects
policypol_Policies
api-keykey_API Keys

Actions

Actions follow the module:Operation pattern. The full list of all action strings per module is in the Permissions Reference.

Action Surface Mapping

Every permission action corresponds to a single operation that is reachable through all four client surfaces. Given actors:CreateActor as an example:

SurfaceConventionExample
Permissionmodule:OperationNameactors:CreateActor
REST endpointMETHOD /api/v1/...POST /api/v1/actors
MCP toolkebab-case operation namecreate-actor
CLI commandsoat <kebab-case>soat create-actor
SDK methodsoat.<module>.<camelCase>()soat.actors.createActor()

A caller is authorised to invoke an operation if — and only if — the resolved policy grants the corresponding permission action. The same check applies regardless of which surface the caller uses.

Wildcards

  • * — matches all actions across all modules
  • module:* — matches all actions in a specific module (e.g., documents:*)

Conditions

Conditions add attribute-based constraints to statements. A condition block maps an operator to one or more key-value pairs that must all evaluate to true.

{
"condition": {
"StringEquals": {
"soat:ResourceTag/environment": "production"
},
"StringLike": {
"soat:ResourceTag/team": "engineering-*"
}
}
}

Condition Operators

OperatorDescription
StringEqualsExact string match
StringNotEqualsNegated exact match
StringLikeGlob pattern match (*, ?)

Condition Keys

KeySourceDescription
soat:ResourceTag/<key>Resource tagsTag value on the target resource
soat:ResourceTypeRequestThe type of the resource being accessed

Condition operators and condition keys are matched by exact string, and both are stored exactly as written — no case conversion is applied to a condition block or to a resource's tags, unlike ordinary API fields. So soat:ResourceTag/cost_center selects the tag cost_center and not costCenter: they are two different tags, and a policy naming one does not match a resource carrying the other. Write the tag key and the condition key the same way and they always agree.

Authorization Model

Authorization in SOAT is policy-only — there is no separate project membership gate. All access decisions are evaluated through the policy engine against the requested action and the target resource SRN.

Policy Resolution by Caller Type

Caller typePolicies used
Admin (JWT)Bypassed — admins have unrestricted access to all resources
Regular user (JWT)All policies attached to the user (via User.policyIds)
API key (no policies)Inherits the owning user's policies, hard-locked to the key's project
API key (with policies)Intersection of user policies and key policies — both must allow the action
OAuth tokenIntersection of user policies and the consented scope, hard-locked to the token's project

Every API key is hard-locked to its project_id, and every OAuth token to its prj; access to any other project is denied regardless of policy — and regardless of the owner's role. An admin owner cannot cross a scoped credential's project boundary for resource operations: admin lifts the policy ceiling within scope and passes the role-gated project create/delete, but never the scope binding itself, so a cross-project resource write still returns 403 API_KEY_PROJECT_SCOPE. See Project scope is a hard boundary, even for admins.

Why Intersection Semantics Matter

When an API key has policies attached — or an OAuth token carries a consented scope — the credential can never exceed the permissions of the user who owns it. Even if the key's policy or the consent is very permissive, the user's policies still apply as a ceiling. This is why both API keys and OAuth tokens are safe to delegate. The same evaluator enforces all credential types.

Authorization by Caller Type

ScenarioResultReason
Admin accessing any resourceAllowedAdmins bypass policy evaluation
User with resource: ["soat:proj_A:*:*"] accessing proj_AAllowedPolicy covers the SRN
User with resource: ["soat:proj_A:*:*"] accessing proj_BDeniedPolicy does not cover proj_B SRN
API key scoped to proj_A, accessing proj_BDeniedKey is hard-locked to proj_A
API key with key policy allowed, but user policy deniedDeniedIntersection semantics — both must allow
API key without policies, accessing resource allowed by user policyAllowedKey inherits user permissions

Policy Evaluation

Policy evaluation (Layer 2) follows AWS IAM semantics:

  1. Default deny — if no statement matches, access is denied.
  2. Explicit deny wins — if any statement explicitly denies, access is denied regardless of allows.
  3. Allow — if at least one statement allows and no statement denies, access is granted.

Statement Matching

A statement matches a request when all of the following are true:

  1. At least one pattern in action matches the requested action.
  2. At least one pattern in resource matches the target SRN (or resource is omitted / ["*"]).
  3. All condition blocks evaluate to true (or condition is omitted).

Pattern Matching

  • * matches everything.
  • module:* matches all actions in a module.
  • soat:proj_ABC:document:* matches all documents in a project.
  • Wildcards apply only at segment boundaries — partial wildcards like doc_X* are not supported.
  • Path-based patterns: when a resource has a path field, the resource ID segment of the SRN may be a logical path. Both the resource's id and its path are tested when evaluating a single-resource check. Glob patterns (/reports/*) are expanded to SQL LIKE for list queries.

Tags

Tags are key-value pairs attached to resources. They enable attribute-based access control (ABAC) via conditions. Taggable resources include documents, files, actors, conversations, and discussions.

{
"tags": {
"environment": "production",
"team": "engineering",
"sensitivity": "high"
}
}

Tags are managed via each resource's create/update endpoints using the tags field, or through dedicated tag sub-endpoints:

PUT /api/v1/<resource>/:id/tags Replace all tags
PATCH /api/v1/<resource>/:id/tags Merge tags
GET /api/v1/<resource>/:id/tags Get tags

Tag keys are stored verbatim

A tag key is an opaque label, not an API field name, so — unlike every other field in the REST API — it is never case-converted. It is stored, returned, and matched against soat:ResourceTag/<key> exactly as you wrote it, on REST, in formation templates, and over MCP alike.

Two consequences worth knowing:

  • cost_center and costCenter are two different tags. A resource can carry both, and a policy naming one does not match a resource carrying only the other.
  • The key you read back is the key to name in a condition. GET .../tags returns the stored key verbatim, so it can be copied straight into soat:ResourceTag/<key>.

Examples

Full Access Policy

Equivalent to unrestricted access across all projects. The resource: ["*"] wildcard matches all SRNs globally.

{
"statement": [
{
"effect": "Allow",
"action": ["*"],
"resource": ["*"]
}
]
}

Project-scoped Read-only Policy

Grants read access to a specific project's resources. Attach this to a user or API key.

{
"statement": [
{
"effect": "Allow",
"action": [
"projects:GetProject",
"documents:GetDocument",
"documents:ListDocuments",
"files:GetFile",
"files:ListFiles"
],
"resource": ["soat:proj_ABC:*:*"]
}
]
}

Read-only Across All Modules (Global)

Grants read access to documents, files, actors, and conversations across all projects. Attach to users who need broad read access.

{
"statement": [
{
"effect": "Allow",
"action": [
"documents:GetDocument",
"documents:ListDocuments",
"knowledge:SearchKnowledge",
"files:GetFile",
"files:DownloadFile",
"actors:ListActors",
"actors:GetActor",
"conversations:ListConversations",
"conversations:GetConversation"
],
"resource": ["*"]
}
]
}

Allow All File Operations Except Delete

{
"statement": [
{
"effect": "Allow",
"action": ["files:*"],
"resource": ["soat:proj_ABC:file:*"]
},
{
"effect": "Deny",
"action": ["files:DeleteFile"],
"resource": ["soat:proj_ABC:file:*"]
}
]
}

Condition-based Access

Allow only actors tagged "internal":

{
"statement": [
{
"effect": "Allow",
"action": ["actors:GetActor"],
"resource": ["soat:proj_ABC:actor:*"],
"condition": {
"StringEquals": {
"soat:ResourceTag/visibility": "internal"
}
}
}
]
}

Users

For user identity management, roles, authentication, and bootstrap, see the Users module.