# EDCS API Integration Guide

> Machine-readable integration spec for the Enterprise Distributed Config System (EDCS).
> Audience: developers and AI agents (e.g. Claude). No credentials appear in this document —
> every secret is shown as a `<placeholder>`.

EDCS is an OAuth2-protected platform of four services:

| Service       | Purpose                                  | OpenAPI (Swagger UI) |
|---------------|------------------------------------------|----------------------|
| Identity STS  | Issues JWT access tokens (OAuth2)        | `{sts}/swagger`       |
| Vault API     | Encrypted secret storage                 | `{vault}/swagger`     |
| AppConfig API | Application configuration & feature flags| `{appconfig}/swagger` |
| Directory API | LDAP-backed users & groups               | `{directory}/swagger` |

Replace `{sts}`, `{vault}`, `{appconfig}`, `{directory}` with your deployment's base URLs.
The authoritative issuer and JWKS are published at the STS discovery document:
`GET {sts}/.well-known/openid-configuration`.

---

## 1. Authentication

All resource APIs require a JWT bearer token issued by the STS token endpoint:

```
POST {sts}/connect/token
Content-Type: application/x-www-form-urlencoded
```

### Grant types

**Password (interactive / human users)** — credentials are validated against the directory;
the granted scopes are derived from the user's group membership (you cannot request scopes
you are not entitled to):

```
grant_type=password
username=<username>
password=<password>
scope=openid
```

**Client credentials (services & AI agents)** — register a client with an allowed-scope set;
the granted scope is `requested ∩ allowed` (never more than the client is permitted):

```
grant_type=client_credentials
client_id=<client-id>
client_secret=<client-secret>
scope=<space-separated-scopes>
```

**Refresh** — exchange a refresh token for a new access token (refresh tokens are single-use):

```
grant_type=refresh_token
refresh_token=<refresh-token>
```

### Token response

```json
{
  "access_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "<opaque-token>",
  "scope": "openid vault:read"
}
```

Send the token on every resource request:

```
Authorization: Bearer <access_token>
```

---

## 2. Scopes

Access is enforced per endpoint. `edcs:admin` is a wildcard that satisfies every scope.

| Scope              | Grants                                             |
|--------------------|----------------------------------------------------|
| `edcs:admin`       | Everything (wildcard)                              |
| `vault:read`       | Read secrets, versions, audit                      |
| `vault:write`      | Write / delete / restore secrets                   |
| `appconfig:read`   | Read config entries, feature flags, audit          |
| `appconfig:write`  | Write / delete config entries                      |
| `directory:read`   | Read users and groups                              |
| `directory:write`  | Create users                                       |
| `directory:admin`  | Delete users, reset passwords, lock, manage groups |

A request with a valid token but insufficient scope returns **403 Forbidden**; a missing or
invalid token returns **401 Unauthorized**.

---

## 3. Endpoint reference

### Identity STS — `{sts}`
| Method | Path                                  | Auth        |
|--------|---------------------------------------|-------------|
| POST   | `/connect/token`                      | none (rate-limited) |
| GET    | `/.well-known/openid-configuration`   | none        |
| GET    | `/.well-known/jwks`                   | none        |

### Vault API — `{vault}`
| Method | Path                              | Scope        |
|--------|-----------------------------------|--------------|
| GET    | `/v1/secrets/{path}`              | `vault:read` |
| PUT    | `/v1/secrets/{path}`              | `vault:write`|
| DELETE | `/v1/secrets/{path}`              | `vault:write`|
| GET    | `/v1/secret-paths`                | `vault:read` |
| GET    | `/v1/secret-paths/deleted`        | `vault:read` |
| GET    | `/v1/secret-versions/{path}`      | `vault:read` |
| GET    | `/v1/secret-summary`              | `vault:read` |
| GET    | `/v1/secret-audit/{path}`         | `vault:read` |
| GET    | `/v1/audit?limit=`                | `vault:read` |
| POST   | `/v1/secret-restore?path=`        | `vault:write`|

`PUT /v1/secrets/{path}` body: `{ "value": "<secret-value>" }`.

### AppConfig API — `{appconfig}`
| Method | Path                                   | Scope            |
|--------|----------------------------------------|------------------|
| GET    | `/v1/apps`                             | `appconfig:read` |
| GET    | `/v1/apps/{appId}/config?label=`       | `appconfig:read` |
| GET    | `/v1/apps/{appId}/features`            | `appconfig:read` |
| GET    | `/v1/apps/{appId}/audit?limit=`        | `appconfig:read` |
| GET    | `/v1/app-config/{appId}/{key}?label=`  | `appconfig:read` |
| PUT    | `/v1/app-config/{appId}/{key}`         | `appconfig:write`|
| DELETE | `/v1/app-config/{appId}/{key}?label=`  | `appconfig:write`|
| GET    | `/v1/config/search?q=&limit=`          | `appconfig:read` |
| GET    | `/v1/vault-refs?path=`                 | `appconfig:read` |

`PUT` body: `{ "value": "...", "label": "", "contentType": "text/plain", "isVaultRef": false, "isFeatureFlag": false }`.

### Directory API — `{directory}`
| Method | Path                          | Scope               |
|--------|-------------------------------|---------------------|
| POST   | `/auth/bind`                  | none (rate-limited) |
| GET    | `/users` `/users/{uid}` `/users/stats` `/users/count` `/users/{uid}/groups` | `directory:read` |
| POST   | `/users`                      | `directory:write`   |
| PUT    | `/users/{uid}`                | `directory:admin`   |
| DELETE | `/users/{uid}`                | `directory:admin`   |
| PUT    | `/users/{uid}/reset-password` | `directory:admin`   |
| PUT    | `/users/{uid}/lock`           | `directory:admin`   |
| POST   | `/users/{uid}/password`       | `directory:admin`   |
| GET    | `/groups` `/groups/{cn}`      | `directory:read`    |
| POST   | `/groups` `/groups/{cn}/members` | `directory:admin` |
| DELETE | `/groups/{cn}` `/groups/{cn}/members/{memberDn}` | `directory:admin` |
| GET    | `/auth/audit?limit=`          | `directory:read`    |

`uid` and group `cn` must be alphanumeric. `POST /users` body:
`{ "uid": "...", "givenName": "...", "sn": "...", "mail": "...", "initialPassword": "...", "locked": false }`.

---

## 4. Examples

### curl — service/agent reads a secret
```bash
TOKEN=$(curl -s -X POST "$STS/connect/token" \
  -d grant_type=client_credentials \
  -d client_id="$CLIENT_ID" \
  -d client_secret="$CLIENT_SECRET" \
  -d scope=vault:read | jq -r .access_token)

curl -s "$VAULT/v1/secrets/myapp/db-password" \
  -H "Authorization: Bearer $TOKEN"
```

### C# (HttpClient)
```csharp
var form = new Dictionary<string,string> {
    ["grant_type"]    = "client_credentials",
    ["client_id"]     = clientId,
    ["client_secret"] = clientSecret,
    ["scope"]         = "appconfig:read",
};
var tok = await http.PostAsync($"{sts}/connect/token", new FormUrlEncodedContent(form));
var token = (await tok.Content.ReadFromJsonAsync<TokenResponse>())!.access_token;

http.DefaultRequestHeaders.Authorization = new("Bearer", token);
var cfg = await http.GetAsync($"{appconfig}/v1/apps/myapp/config");
```

### Python (AI agent)
```python
import requests, os
tok = requests.post(f"{STS}/connect/token", data={
    "grant_type": "client_credentials",
    "client_id": os.environ["EDCS_CLIENT_ID"],
    "client_secret": os.environ["EDCS_CLIENT_SECRET"],
    "scope": "vault:read appconfig:read",
}).json()["access_token"]

r = requests.get(f"{VAULT}/v1/secret-paths",
                 headers={"Authorization": f"Bearer {tok}"})
print(r.json())
```

> **For AI agents (Claude):** prefer the `client_credentials` grant with the narrowest scope
> the task needs. Tokens are short-lived (~5–15 min); request a fresh one rather than caching
> long-term. Read each service's `/swagger/v1/swagger.json` for the full machine-readable
> OpenAPI schema. Never log or echo token values or secret contents.

---

## 5. MCP server (for AI agents)

EDCS ships a Model Context Protocol server that exposes read-only tools over the same APIs,
authenticating to EDCS with a scoped `edcs-mcp` service identity. Two transports:

- **stdio** — run locally by Claude Code / Desktop (see `.mcp.json` in the repo root). The
  developer's own machine; may request `vault:read` to read secret values locally.
- **http** — Streamable HTTP at `POST {mcp}/mcp` (prod: `https://edcs-mcp.securitasmachina.org/mcp`).
  **Requires an API key**: send `Authorization: Bearer <EDCS_MCP_API_KEY>` on every request
  (401 otherwise). The remote deployment runs with **`appconfig:read directory:read` only — no
  `vault:read`** (secret values are never piped through the network endpoint). The server refuses
  to start in http mode without an API key configured.

Tools: `vault_list_paths`, `vault_read_secret`, `vault_list_versions`, `app_config_list_apps`,
`app_config_get_config`, `app_config_get_value`, `app_config_search`, `directory_list_users`,
`directory_get_user`, `directory_list_groups`. The active service scopes bound what these tools
can return (vault tools work only when the server holds `vault:read`, i.e. stdio).

---

## 6. Operational notes

- **Rate limiting:** `/connect/token`, `/auth/bind`, and the sign-in / self-registration
  endpoints are throttled per client IP. Expect **429 Too Many Requests** when exceeded.
- **Errors:** OAuth errors follow `{ "error": "...", "error_description": "..." }`. Resource
  errors are standard HTTP status codes (400/401/403/404).
- **Self-registration:** new accounts are created **locked** and require administrator
  approval before they can sign in.
- **Secrets hygiene:** never commit tokens or secret values; never include them in logs,
  URLs, or error messages.
