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)
Related Tutorials
- Permissions in Practice - Step 4 (Create policies)
- Permissions in Practice - Step 6 (Create API keys)
- Permissions in Practice - Step 7 (Verify permissions)
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 can optionally be scoped to a single project and/or 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
effect | string | Yes | "Allow" or "Deny" |
action | string[] | Yes | Actions this statement applies to (supports wildcards) |
resource | string[] | No | SRNs this statement applies to (default: ["*"]) |
condition | object | No | Conditions 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.
SOAT Resource Names (SRNs)
Every addressable entity has a canonical identifier called a SOAT Resource Name:
soat:<project_id>:<resource_type>:<resource_id>
Examples:
| SRN | Description |
|---|---|
soat:proj_ABC:document:doc_XYZ | A specific document |
soat:proj_ABC:document:* | All documents in a project |
soat:proj_ABC:file:* | All files in a project |
soat:proj_ABC:actor:act_123 | A 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 inproj_ABConly.resource: ["soat:*:document:*"]— matches all documents across all projects.
To give a user or API key access to a specific project without scoping the key via project_id, create a policy with resource: ["soat:proj_ABC:*:*"]. This achieves project-level scoping entirely through the policy engine, without creating a dedicated API key per project.
Resource Types
| Resource Type | Public ID Prefix | Module |
|---|---|---|
document | doc_ | Documents |
file | file_ | Files |
actor | act_ | Actors |
conversation | conv_ | Conversations |
project | proj_ | Projects |
policy | pol_ | Policies |
api-key | key_ | 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:
| Surface | Convention | Example |
|---|---|---|
| Permission | module:OperationName | actors:CreateActor |
| REST endpoint | METHOD /api/v1/... | POST /api/v1/actors |
| MCP tool | kebab-case operation name | create-actor |
| CLI command | soat <kebab-case> | soat create-actor |
| SDK method | soat.<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 modulesmodule:*— 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
| Operator | Description |
|---|---|
StringEquals | Exact string match |
StringNotEquals | Negated exact match |
StringLike | Glob pattern match (*, ?) |
Condition Keys
| Key | Source | Description |
|---|---|---|
soat:ResourceTag/<key> | Resource tags | Tag value on the target resource |
soat:ResourceType | Request | The type of the resource being accessed |
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 type | Policies 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 |
| API key (with policies) | Intersection of user policies and key policies — both must allow the action |
| API key (with project_id) | Same as above, but hard-locked to that project regardless of policy |
Why Intersection Semantics Matter
When an API key has policies attached, the key can never exceed the permissions of the user who owns it. Even if the key's policy is very permissive, the user's policies still apply as a ceiling. This allows safely delegating a scoped subset of permissions without risk of escalation.
Authorization by Caller Type
| Scenario | Result | Reason |
|---|---|---|
| Admin accessing any resource | Allowed | Admins bypass policy evaluation |
User with resource: ["soat:proj_A:*:*"] accessing proj_A | Allowed | Policy covers the SRN |
User with resource: ["soat:proj_A:*:*"] accessing proj_B | Denied | Policy does not cover proj_B SRN |
| API key scoped to proj_A, accessing proj_B | Denied | Key is hard-locked to proj_A |
| API key with key policy allowed, but user policy denied | Denied | Intersection semantics — both must allow |
| API key without policies, accessing resource allowed by user policy | Allowed | Key inherits user permissions |
Policy Evaluation
Policy evaluation (Layer 2) follows AWS IAM semantics:
- Default deny — if no statement matches, access is denied.
- Explicit deny wins — if any statement explicitly denies, access is denied regardless of allows.
- 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:
- At least one pattern in
actionmatches the requested action. - At least one pattern in
resourcematches the target SRN (orresourceis omitted /["*"]). - All
conditionblocks evaluate to true (orconditionis 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
pathfield, the resource ID segment of the SRN may be a logical path. Both the resource'sidand itspathare tested when evaluating a single-resource check. Glob patterns (/reports/*) are expanded to SQLLIKEfor 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, and conversations.
{
"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
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.
{
"version": "2025-01-01",
"statement": [
{
"effect": "Allow",
"action": [
"documents:GetDocument",
"documents:ListDocuments",
"documents:SearchDocuments",
"files:GetFile",
"files:DownloadFile",
"actors:ListActors",
"actors:GetActor",
"conversations:ListConversations",
"conversations:GetConversation"
],
"resource": ["*"]
}
]
}
Allow All File Operations Except Delete
{
"version": "2025-01-01",
"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":
{
"version": "2025-01-01",
"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.