# Configuration & Guides

## Configuration Reference

All options are set under `config :ex_scim`.

### Core

| Key | Default | Description |
|-----|---------|-------------|
| `:base_url` | `"http://localhost:4000"` | Base URL for SCIM endpoints. Resource URLs are built as `<base_url>/scim/v2/...`, so the routes are expected under `/scim/v2`. Falls back to `SCIM_BASE_URL` env var. |
| `:storage_strategy` | `ExScim.Storage.EtsStorage` | Module implementing `ExScim.Storage.Adapter` |
| `:auth_provider_adapter` | *required* | Module implementing `ExScim.Auth.AuthProvider.Adapter` |
| `:authorization_adapter` | `ExScim.Authorization.DefaultPolicy` | Module implementing `ExScim.Authorization.Adapter` |

### Resource Mapping

| Key | Default | Description |
|-----|---------|-------------|
| `:user_resource_mapper` | `ExScim.Users.Mapper.DefaultMapper` | Module implementing `ExScim.Users.Mapper.Adapter` |
| `:group_resource_mapper` | `ExScim.Groups.Mapper.DefaultMapper` | Module implementing `ExScim.Groups.Mapper.Adapter` |

### Ecto Storage (`ex_scim_ecto`)

| Key | Default | Description |
|-----|---------|-------------|
| `:storage_repo` | *required* | Your Ecto `Repo` module |
| `:user_model` | *required* | Ecto schema module or `{Schema, opts}` tuple |
| `:group_model` | *required* | Same format as `:user_model` |
| `:membership_model` | `nil` | Ecto schema storing group memberships, or `{Schema, opts}` |

When using the `{Schema, opts}` tuple form, available sub-options are:

- `:preload` - list of associations to preload (default `[]`)
- `:lookup_key` - primary key field (default `:id`)
- `:filter_mapping` - map of SCIM attribute paths to DB columns (default `%{}`)
- `:tenant_key` - column used for multi-tenant scoping (default `nil`)
- `:field_mapping` - map of domain fields to `{db_field, to_storage_fn, from_storage_fn}` tuples for value transformation (default `%{}`)

```elixir
config :ex_scim,
  storage_strategy: ExScimEcto.StorageAdapter,
  storage_repo: MyApp.Repo,
  user_model:
    {MyApp.Accounts.User,
     preload: [:roles],
     lookup_key: :uuid,
     filter_mapping: %{"emails.value" => :email},
     tenant_key: :organization_id,
     field_mapping: %{
       active: {:status,
         fn true -> "active"; false -> "inactive" end,
         fn "active" -> true; _ -> false end}
     }},
  group_model: {MyApp.Groups.Group, tenant_key: :organization_id},
  membership_model: MyApp.Groups.Membership
```

### Group Memberships

Adapters that implement the membership callbacks of `ExScim.Storage.Adapter` store `members` and derive `User.groups`.

- Members are users or groups.
- Unknown members and cycles return `400 invalidValue`.
- `User.groups` lists memberships as `direct` or `indirect`.
- Deleting a user or group removes its memberships.

`ExScim.Storage.EtsStorage` supports memberships, and `ExScimEcto.StorageAdapter` does when `:membership_model` is set.

### Capabilities

Reported in the ServiceProviderConfig discovery endpoint.

| Key | Default |
|-----|---------|
| `:patch_supported` | `false` |
| `:bulk_supported` | `true` |
| `:bulk_max_operations` | `1000` |
| `:bulk_max_payload_size` | `1_048_576` |
| `:filter_supported` | `false` |
| `:filter_max_results` | `200` |
| `:sort_supported` | `false` |
| `:change_password_supported` | `false` |
| `:etag_supported` | `false` |

### Discovery

| Key | Default | Description |
|-----|---------|-------------|
| `:documentation_uri` | `nil` | URI included in ServiceProviderConfig |
| `:authentication_schemes` | `[]` | List of scheme maps per RFC 7643 |
| `:resource_types` | User + Group | List of resource type maps |
| `:schema_modules` | Built-in User, EnterpriseUser, Group | Schema definition modules |

### Lifecycle & Tenancy

| Key | Default | Description |
|-----|---------|-------------|
| `:lifecycle_adapter` | `nil` | Module implementing `ExScim.Lifecycle.Adapter` |
| `:tenant_resolver` | `nil` | Module implementing `ExScim.Tenant.Resolver` |

## Authorization Scopes

Scopes are strings in the `ExScim.Scope` struct's `:scopes` list, populated by your `AuthProvider.Adapter` when validating a token or credentials. Every endpoint and every bulk operation asks the configured authorization policy whether the scope may perform an action (`:read`, `:create`, `:update`, `:delete`) on a resource (`:users`, `:groups`, `:me`, `:schemas`, `:resource_types`, `:service_provider_config`).

The default policy, `ExScim.Authorization.DefaultPolicy`, expects the `scim:*` scopes below. To use the scopes your identity platform issues instead, see [Custom policies](#custom-policies).

### Standard scopes

| Scope | Endpoints | Actions |
|---|---|---|
| `scim:read` | `/Users`, `/Groups`, `/Schemas`, `/ServiceProviderConfig` | GET (list, show, search) |
| `scim:create` | `/Users`, `/Groups`, `/Bulk` (POST operations) | POST |
| `scim:update` | `/Users`, `/Groups`, `/Bulk` (PUT/PATCH operations) | PUT, PATCH |
| `scim:delete` | `/Users`, `/Groups`, `/Bulk` (DELETE operations) | DELETE |

### `/Me` scopes

| Scope | Action |
|---|---|
| `scim:me:read` | GET `/Me` |
| `scim:me:create` | POST `/Me` |
| `scim:me:update` | PUT `/Me`, PATCH `/Me` |
| `scim:me:delete` | DELETE `/Me` |

### Bulk operations

For `/Bulk`, scope is enforced per individual operation rather than on the request as a whole. A caller with only `scim:create` may submit a bulk request containing POST operations; any PUT, PATCH, or DELETE operations in the same payload will return a `403` operation result (and count toward `failOnErrors`).

### Example scope lists

```elixir
# Read-only client
scopes: ["scim:read"]

# Provisioning client - can create and update users, but not delete them
scopes: ["scim:read", "scim:create", "scim:update"]

# Full-access admin client
scopes: ["scim:read", "scim:create", "scim:update", "scim:delete"]

# Self-service user (Me endpoint only)
scopes: ["scim:me:read", "scim:me:update"]
```

`/ResourceTypes` requires no scope.

### Custom policies

Implement `ExScim.Authorization.Adapter` to authorize against other scope names, authorize Users and Groups separately, or decide based on token claims in `scope.metadata`:

```elixir
defmodule MyApp.ScimPolicy do
  @behaviour ExScim.Authorization.Adapter

  alias ExScim.Authorization.DefaultPolicy
  alias ExScim.Scope

  @impl true
  def authorize(scope, resource, :read) when resource in [:users, :groups],
    do: require_scope(scope, "directory.#{resource}.read")

  def authorize(scope, resource, _action) when resource in [:users, :groups],
    do: require_scope(scope, "directory.#{resource}.write")

  def authorize(scope, resource, action), do: DefaultPolicy.authorize(scope, resource, action)

  defp require_scope(scope, required) do
    if Scope.has_scope?(scope, required),
      do: :ok,
      else: {:error, {:missing_scopes, [required]}}
  end
end
```

```elixir
config :ex_scim, authorization_adapter: MyApp.ScimPolicy
```

Denials return `403`. `{:error, {:missing_scopes, scopes}}` lists the missing scopes in the error detail; any other reason yields a generic detail. Cross-resource search (`POST /.search`) requires `:read` on both `:users` and `:groups`.

## Multi-Tenancy

Multi-tenancy is opt-in. When no `tenant_resolver` is configured (or `scope.tenant_id` is `nil`), the system operates in single-tenant mode with no isolation applied.

To enable it, wire up three pieces:

1. **Tenant resolver** - implement `ExScim.Tenant.Resolver` to extract a tenant identifier from the request (header, subdomain, path, etc.).
2. **Phoenix plug** - add `ExScimPhoenix.Plugs.ScimTenant` to your SCIM pipeline after `ScimAuth`.
3. **Ecto tenant key** - set `:tenant_key` on your model tuples so queries are scoped and creates inject the tenant ID.

```elixir
# 1. Resolver
defmodule MyApp.TenantResolver do
  @behaviour ExScim.Tenant.Resolver

  @impl true
  def resolve_tenant(conn, _scope) do
    case Plug.Conn.get_req_header(conn, "x-tenant-id") do
      [tenant_id] -> {:ok, tenant_id}
      _ -> {:error, :missing_tenant}
    end
  end
end

# 2. Config
config :ex_scim,
  tenant_resolver: MyApp.TenantResolver,
  user_model: {MyApp.Accounts.User, tenant_key: :organization_id},
  group_model: {MyApp.Groups.Group, tenant_key: :organization_id}

# 3. Pipeline
#    Add: plug ExScimPhoenix.Plugs.ScimTenant
```

The resolver can optionally implement `tenant_scim_base_url/1` to generate tenant-specific resource location URLs (e.g., `https://acme.example.com/scim/v2`).

## Custom Adapters

### Storage Adapter

Implement the `ExScim.Storage.Adapter` behaviour to use a custom data store:

```elixir
defmodule MyApp.CustomStorage do
  @behaviour ExScim.Storage.Adapter

  def get_user(id), do: # your implementation
  def create_user(user_data), do: # your implementation
  # ... other callbacks
end
```

### Resource Mapper

Implement `ExScim.Users.Mapper.Adapter` or `ExScim.Groups.Mapper.Adapter` to control how domain structs map to SCIM JSON:

```elixir
defmodule MyApp.UserMapper do
  use ExScim.Users.Mapper.Adapter

  @impl true
  def from_scim(scim_data, _caller) do
    {:ok,
     %MyApp.User{
       username: scim_data["userName"],
       email: get_primary_email(scim_data["emails"])
     }}
  end

  @impl true
  def to_scim(%MyApp.User{} = user, _caller, opts \\ []) do
    {:ok,
     %{
       "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"],
       "id" => user.id,
       "userName" => user.username,
       "emails" => format_emails(user.email),
       "meta" => format_meta(user, opts)
     }}
  end
end
```

`opts` carries `:location`, the resource URL. Passing `opts` to `format_meta/2` sets `meta.location`, which is also returned as the `Location` header.

## Endpoints

All endpoints are served under the scope you configure (typically `/scim/v2`).

### Users

| Method | Path | Description | RFC |
|--------|------|-------------|-----|
| `GET` | `/Users` | List with filtering, sorting, pagination | [RFC 7644 Section 3.4.2](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2) |
| `POST` | `/Users` | Create | [RFC 7644 Section 3.3](https://www.rfc-editor.org/rfc/rfc7644#section-3.3) |
| `GET` | `/Users/{id}` | Fetch by ID | [RFC 7644 Section 3.4.1](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1) |
| `PUT` | `/Users/{id}` | Replace | [RFC 7644 Section 3.5.1](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.1) |
| `PATCH` | `/Users/{id}` | Partial update (JSON Patch) | [RFC 7644 Section 3.5.2](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2) |
| `DELETE` | `/Users/{id}` | Delete | [RFC 7644 Section 3.6](https://www.rfc-editor.org/rfc/rfc7644#section-3.6) |

Groups and Me follow the same pattern. See [RFC 7644 Section 3.11](https://www.rfc-editor.org/rfc/rfc7644#section-3.11) for Me endpoint details.

### Other

| Method | Path | Description | RFC |
|--------|------|-------------|-----|
| `POST` | `/.search` | Cross-resource search | [RFC 7644 Section 3.4.3](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.3) |
| `POST` | `/Bulk` | Bulk operations | [RFC 7644 Section 3.7](https://www.rfc-editor.org/rfc/rfc7644#section-3.7) |
| `GET` | `/ServiceProviderConfig` | Server capabilities | [RFC 7644 Section 4](https://www.rfc-editor.org/rfc/rfc7644#section-4) |
| `GET` | `/ResourceTypes` | Supported resource types | [RFC 7644 Section 4](https://www.rfc-editor.org/rfc/rfc7644#section-4) |
| `GET` | `/Schemas` | Schema definitions | [RFC 7644 Section 4](https://www.rfc-editor.org/rfc/rfc7644#section-4) |
