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 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
| 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. 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:
| 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:actor_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 (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 Type | Public ID Prefix | Module |
|---|---|---|
document | doc_ | Documents |
file | file_ | Files |
actor | actor_ | 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 |
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 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, hard-locked to the key's project |
| API key (with policies) | Intersection of user policies and key policies — both must allow the action |
| OAuth token | Intersection 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
| 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, 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_centerandcostCenterare 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 .../tagsreturns the stored key verbatim, so it can be copied straight intosoat: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.