Skip to main content

Permissions in Practice

This tutorial walks through a realistic permissions setup from scratch. You will:

  1. Log in as admin.
  2. Create two regular users — alice (project lead) and bob (read-only analyst).
  3. Create a project called Analytics.
  4. Define two policies: one that grants full access to the project, and one that only allows read operations.
  5. Attach policies to each user.
  6. Create project-scoped API keys for both users, each with its own policy constraint.
  7. Verify what each key can and cannot do.

By the end you will understand how policies, users, and API keys compose together to produce fine-grained access control.

Prerequisites

  • SOAT running locally. Follow the Quick Start guide to bring the stack up with Docker Compose.
  • New to SOAT? Read Key Concepts to understand projects, users, and the IAM model before diving in.
  • CLI installed and configured, or SDK set up. See CLI or SDK.
  • For production hardening (secrets, env vars), see Advanced Configuration.
  • Server is at http://localhost:5047.

Export your server URL (used in subsequent steps):

export SOAT_BASE_URL=http://localhost:5047

CLI path flags in this tutorial are resource-specific and kebab-cased, for example --user-id and --project-id.


Step 1 — Log in as admin

Admin is the built-in superuser role. It bypasses policy evaluation entirely. See IAM — Authentication for details on JWT tokens and the admin role.

soat login-user --username admin --password Admin1234!

The CLI prints a token. Save it and update your profile:

soat configure
# Token: <paste token here>

Step 2 — Create regular users

Create alice (project lead) and bob (read-only analyst). Only admins can create users. See Users for the full user management reference.

ALICE_ID=$(soat create-user --username alice --password Alice1234! | jq -r '.id')
BOB_ID=$(soat create-user --username bob --password Bob1234! | jq -r '.id')
echo "alice: $ALICE_ID"
echo "bob : $BOB_ID"

Note the id field (usr_…) for each user — you will need them when attaching policies.


Step 3 — Create the Analytics project

See Projects for the full project management reference.

PROJECT_ID=$(soat create-project --name "Analytics" | jq -r '.id')
echo "project: $PROJECT_ID"

Copy the returned id (e.g. proj_…).


Step 4 — Create policies

You will create two policies. Replace $PROJECT_ID with the actual value from the previous step.

Policies are global (not scoped to any project). The resource field uses SOAT Resource Names (SRNs) to restrict which projects a policy covers. For the full policy document format and evaluation rules, see IAM — Policy Documents.

4a — Full-access policy (for Alice)

This policy allows all actions on every resource inside the Analytics project.

FULL_POLICY_ID=$(soat create-policy \
--name "analytics-full-access" \
--description "Full access to the Analytics project" \
--document '{
"statement": [
{
"effect": "Allow",
"action": ["*"],
"resource": ["soat:'"$PROJECT_ID"':*:*"]
}
]
}' | jq -r '.id')
echo "full-access policy: $FULL_POLICY_ID"

4b — Read-only policy (for Bob)

This policy only allows read actions on files inside the project. For the full list of files:* and documents:* actions, see the Permissions Reference.

READ_POLICY_ID=$(soat create-policy \
--name "analytics-read-only" \
--description "Read-only access to files and documents in Analytics" \
--document '{
"statement": [
{
"effect": "Allow",
"action": [
"files:GetFile",
"documents:ListDocuments",
"documents:GetDocument"
],
"resource": ["soat:'"$PROJECT_ID"':*:*"]
}
]
}' | jq -r '.id')
echo "read-only policy: $READ_POLICY_ID"

Step 5 — Attach policies to users

Attach the full-access policy to Alice and the read-only policy to Bob. See Policies — Attaching Policies to Users for more details.

note

PUT /users/:user_id/policies replaces the user's entire policy list with the provided array.

# Give Alice full access
soat attach-user-policies \
--user-id "$ALICE_ID" \
--policy-ids '["'"$FULL_POLICY_ID"'"]'

# Give Bob read-only access
soat attach-user-policies \
--user-id "$BOB_ID" \
--policy-ids '["'"$READ_POLICY_ID"'"]'

Step 6 — Create API keys

API keys allow programmatic access without sending a username and password. See API Keys for key rotation and revocation. Here you create two keys:

  • Alice's key — scoped to the Analytics project, inherits her full-access policy.
  • Bob's key — scoped to the Analytics project, further restricted to the read-only policy.

First, log in as each user to obtain their JWT tokens.

# Log in as Alice and save her token to a named profile
soat login-user --username alice --password Alice1234!
soat configure --profile alice

# Log in as Bob and save his token
soat login-user --username bob --password Bob1234!
soat configure --profile bob

# Create Alice's project key (using her profile)
ALICE_API_KEY=$(soat --profile alice create-api-key \
--name "alice-analytics-key" \
--project-id "$PROJECT_ID" | jq -r '.key')

# Create Bob's project key, explicitly restricting it to the read-only policy
BOB_API_KEY=$(soat --profile bob create-api-key \
--name "bob-analytics-key" \
--project-id "$PROJECT_ID" \
--policy-ids '["'"$READ_POLICY_ID"'"]' | jq -r '.key')

echo "Alice key: $ALICE_API_KEY"
echo "Bob key : $BOB_API_KEY"
warning

Store the key value. The raw sk_… key is returned only once. Store it in a secret manager or environment variable immediately. There is no way to retrieve it again — if lost, delete the key and create a new one.


Step 7 — Verify permissions

Confirm that each key behaves as expected. The file upload and list operations used here are part of the Files module.

Alice can upload a file

echo "hello world" > sample.txt

SOAT_TOKEN="$ALICE_API_KEY" soat upload-file-base64 \
--project-id "$PROJECT_ID" \
--content "$(base64 -w 0 sample.txt)" \
--filename "sample.txt"

# Switch to Bob's profile to test his permissions
# → expect-fail
SOAT_TOKEN="$BOB_API_KEY" soat upload-file-base64 \
--project-id "$PROJECT_ID" \
--content "$(base64 -w 0 sample.txt)" \
--filename "sample.txt"

Bob can read files

# Bob can list files — read is allowed
SOAT_TOKEN="$BOB_API_KEY" soat list-files --project-id "$PROJECT_ID"

Bob's key cannot exceed Bob's own permissions

Even if you tried to assign FULL_POLICY_ID to Bob's API key, it would not grant more than what Bob's user policies already allow. The effective permissions are always the intersection. See IAM — Authorization Model for the full rules.

# Attempt to create a key for Bob with the full-access policy
# Key is created, but when used it is still limited to Bob's read-only permissions
# because Bob's user policies are the ceiling.
ESCALATED_KEY=$(soat --profile bob create-api-key \
--name "bob-escalation-attempt" \
--project-id "$PROJECT_ID" \
--policy-ids '["'"$FULL_POLICY_ID"'"]' | jq -r '.key')

# → expect-fail
SOAT_TOKEN="$ESCALATED_KEY" soat upload-file-base64 \
--project-id "$PROJECT_ID" \
--content "$(base64 -w 0 sample.txt)" \
--filename "sample.txt"

What you learned

ConceptTakeaway
BootstrapThe first user is created via a special one-shot endpoint.
Admin vs. regular userAdmins bypass the policy engine. Regular users need explicit Allow statements.
Policies are globalPolicies live outside projects. SRNs carry the project ID to scope access.
User → policy attachmentAttach one or more policies to a user; all are evaluated together.
API key scopingA key with project_id is hard-locked to that project.
Intersection semanticsA key's policy_ids narrow permissions — they can never exceed the owning user's policies.
One-time keyThe raw sk_… key is returned only on creation. Store it immediately.

Next steps

  • Explore IAM for the full policy document format including wildcard actions and SRN patterns.
  • See Policies for the complete policy CRUD API.
  • See API Keys for key rotation and revocation patterns.