---
title: Policies
path: api-guides/policies
status: published
---

# Policies

Create, bind, and test access policies. For the conceptual model, see [Policies and Permissions](../core-concepts/policies-and-permissions).

**Base path:** `/v1/policies/`

## Create a policy

```bash
curl -X POST https://scaivault.scailabs.ai/v1/policies \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "production-read-only",
    "description": "Developers can read production secrets from VPN with MFA",
    "rules": [
      {
        "path_pattern": "environments/production/**",
        "permissions": ["read", "list"],
        "conditions": {
          "ip_ranges": ["10.0.0.0/8"],
          "require_mfa": true
        }
      },
      {
        "path_pattern": "shared/certificates/*",
        "permissions": ["read"]
      }
    ]
  }'
```

```python
resp = httpx.post(
    "https://scaivault.scailabs.ai/v1/policies",
    headers={"Authorization": f"Bearer {os.environ['SCAIVAULT_TOKEN']}"},
    json={
        "name": "production-read-only",
        "rules": [
            {
                "path_pattern": "environments/production/**",
                "permissions": ["read", "list"],
                "conditions": {
                    "ip_ranges": ["10.0.0.0/8"],
                    "require_mfa": True,
                },
            },
        ],
    },
)
```

```typescript
const resp = await fetch("https://scaivault.scailabs.ai/v1/policies", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIVAULT_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "production-read-only",
    rules: [
      {
        path_pattern: "environments/production/**",
        permissions: ["read", "list"],
        conditions: { ip_ranges: ["10.0.0.0/8"], require_mfa: true },
      },
    ],
  }),
});
```

Response:

```json
{
  "id": "pol_xyz789",
  "name": "production-read-only",
  "description": "...",
  "rules": [...],
  "is_active": true,
  "created_at": "2026-04-23T14:00:00Z"
}
```

## Rule fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `path_pattern` | string | Yes | Glob pattern (`app/**`, `secrets/*.key`) |
| `permissions` | array | Yes | One or more of `read`, `write`, `delete`, `list`, `rotate`, `admin` |
| `conditions.ip_ranges` | array | No | CIDR ranges the caller IP must be in |
| `conditions.require_mfa` | boolean | No | Token must have fresh MFA |
| `conditions.time_window.start` | string | No | `HH:MM` UTC, inclusive |
| `conditions.time_window.end` | string | No | `HH:MM` UTC, exclusive |

## Bind an identity

A policy without bindings has no effect. Bind it to a group, service account, or user:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/policies/pol_xyz789/bindings \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "identity_type": "group",
    "identity_id": "group:developers"
  }'
```

```python
httpx.post(
    f"https://scaivault.scailabs.ai/v1/policies/pol_xyz789/bindings",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={"identity_type": "group", "identity_id": "group:developers"},
)
```

```typescript
await fetch(`https://scaivault.scailabs.ai/v1/policies/pol_xyz789/bindings`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ identity_type: "group", identity_id: "group:developers" }),
});
```

### Binding fields

| Field | Values |
|-------|--------|
| `identity_type` | `user`, `service_account`, `group` |
| `identity_id` | ScaiKey identifier (`user:alice@acme.example`, `sa:reporting-service`, `group:developers`) |
| `expires_at` (optional) | Auto-expire the binding at this ISO timestamp |

## List policies

```bash
curl -H "Authorization: Bearer $TOKEN" \
     "https://scaivault.scailabs.ai/v1/policies?limit=50"
```

Response:

```json
{
  "policies": [
    {
      "id": "pol_xyz789",
      "name": "production-read-only",
      "rule_count": 2,
      "binding_count": 1,
      "is_active": true
    }
  ],
  "cursor": null,
  "has_more": false
}
```

## Get a policy

```bash
curl -H "Authorization: Bearer $TOKEN" \
     https://scaivault.scailabs.ai/v1/policies/pol_xyz789
```

Returns the full policy including rules and bindings.

## Update a policy

```bash
curl -X PUT https://scaivault.scailabs.ai/v1/policies/pol_xyz789 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "production-read-only",
    "rules": [
      {
        "path_pattern": "environments/production/**",
        "permissions": ["read", "list"],
        "conditions": {"require_mfa": true}
      }
    ]
  }'
```

`PUT` replaces the whole policy body. Rules and metadata not included are dropped. Bindings are preserved.

## Delete a policy

```bash
curl -X DELETE https://scaivault.scailabs.ai/v1/policies/pol_xyz789 \
  -H "Authorization: Bearer $TOKEN"
```

If the policy has active bindings, you get `409 policy_in_use`. Add `?force=true` to remove bindings and delete.

## Test access

Dry-run access checks — useful before deploying a policy change, or from CI.

```bash
curl -X POST https://scaivault.scailabs.ai/v1/policies/test \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "identity_id": "user:alice@acme.example",
    "path": "environments/production/salesforce/api-credentials",
    "permission": "read",
    "context": {
      "source_ip": "10.0.1.50",
      "mfa_verified": true
    }
  }'
```

Response:

```json
{
  "allowed": true,
  "matching_policies": [
    {
      "id": "pol_xyz789",
      "name": "production-read-only",
      "matching_rule_index": 0
    }
  ]
}
```

When denied, the response tells you why:

```json
{
  "allowed": false,
  "reason": "no matching rule",
  "evaluated_policies": ["pol_default", "pol_developer_read"]
}
```

or

```json
{
  "allowed": false,
  "reason": "condition_failed",
  "failed_condition": "require_mfa",
  "matching_rule": {"policy_id": "pol_xyz789", "rule_index": 0}
}
```

## CI pattern

Assert that critical accesses stay allowed after policy edits.

```python
assertions = [
    ("sa:reporting-service", "integrations/salesforce/oauth", "read", True),
    ("sa:reporting-service", "infra/db/primary/root", "read", False),
    ("user:alice@acme.example", "environments/production/**", "read", True),
]

for identity, path, permission, expected in assertions:
    resp = httpx.post(
        "https://scaivault.scailabs.ai/v1/policies/test",
        headers={"Authorization": f"Bearer {CI_TOKEN}"},
        json={"identity_id": identity, "path": path, "permission": permission},
    )
    result = resp.json()
    assert result["allowed"] == expected, f"{identity} {permission} on {path}: expected {expected}, got {result}"
```

Run this in CI to catch access regressions before they merge.

## List bindings for a policy

```bash
curl -H "Authorization: Bearer $TOKEN" \
     https://scaivault.scailabs.ai/v1/policies/pol_xyz789/bindings
```

## Remove a binding

```bash
curl -X DELETE https://scaivault.scailabs.ai/v1/policies/pol_xyz789/bindings/bind_abc \
  -H "Authorization: Bearer $TOKEN"
```

## Partner-scoped policies

Partner admins can create policies that apply to partner-scoped paths:

```json
{
  "name": "partner-shared-read",
  "scope": "partner",
  "rules": [
    {
      "path_pattern": "shared/**",
      "permissions": ["read"]
    }
  ]
}
```

Bindings on partner-scoped policies bind to partner identities (e.g., `partner-admin`).

## What's next

- [Policies and Permissions](../core-concepts/policies-and-permissions) — the conceptual model.
- [Multi-tenancy](../core-concepts/multi-tenancy) — how tenants and partners scope access.
- [Policies Reference](../reference/policies) — complete endpoint spec.
