@tideorg/mcp 1.4.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/GAP_REGISTER.md +18 -8
- package/PRIVACY.md +41 -0
- package/README.md +204 -197
- package/adapters/AGENTS.md +3 -2
- package/adapters/CLAUDE.md +1 -1
- package/canon/anti-patterns.md +55 -62
- package/canon/concepts.md +46 -34
- package/canon/custom-contracts.md +12 -8
- package/canon/feature-mapping.md +27 -75
- package/canon/framework-matrix.md +69 -22
- package/canon/hosting-options.md +111 -0
- package/canon/iga-change-requests-api.md +211 -0
- package/canon/invariants.md +33 -35
- package/canon/security-gap-mapping.md +506 -0
- package/canon/security-runtime-probes.md +177 -0
- package/canon/tidecloak-bootstrap.md +21 -16
- package/canon/tidecloak-endpoints.md +60 -98
- package/canon/troubleshooting.md +115 -53
- package/canon/version-policy.md +8 -12
- package/mcp-server/dist/server.js +221 -19
- package/package.json +48 -45
- package/playbooks/add-auth-nextjs-existing.md +33 -17
- package/playbooks/add-auth-nextjs-fresh.md +1 -1
- package/playbooks/add-rbac-nextjs.md +7 -2
- package/playbooks/bootstrap-realm-from-template.md +33 -18
- package/playbooks/deploy-tidecloak-docker.md +31 -28
- package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
- package/playbooks/initialize-admin-and-link-account.md +22 -19
- package/playbooks/protect-api-nextjs.md +40 -1
- package/playbooks/protect-aspnet-core-asgard.md +313 -0
- package/playbooks/protect-routes-nextjs.md +24 -10
- package/playbooks/provision-tidecloak-skycloak.md +213 -0
- package/playbooks/setup-forseti-e2ee.md +32 -50
- package/playbooks/setup-iga-admin-panel.md +113 -171
- package/playbooks/verify-jwt-server-side.md +20 -1
- package/prompts/build-private-customer-portal.md +1 -1
- package/prompts/security-gap-analysis.md +55 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
- package/reference-apps/encrypted-communication/scenario.md +2 -2
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
- package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
- package/reference-apps/iga-admin-governance/scenario.md +15 -13
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
- package/skills/tide-diagnostics/SKILL.md +1 -1
- package/skills/tide-integration/SKILL.md +9 -18
- package/skills/tide-mcp-qa/SKILL.md +139 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
- package/skills/tide-reviewer/SKILL.md +1 -1
- package/skills/tide-security-analyst/SKILL.md +182 -0
- package/skills/tide-setup/SKILL.md +1 -1
- package/canon/delegation.md +0 -195
- package/playbooks/setup-server-delegation.md +0 -142
|
@@ -411,10 +411,12 @@ If route protection broken:
|
|
|
411
411
|
|
|
412
412
|
1. **Verify auth works**:
|
|
413
413
|
```typescript
|
|
414
|
-
// Add to protected page
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
// Add to protected page.
|
|
415
|
+
// Note: useTideCloak() has no `user` object — read claims via
|
|
416
|
+
// getValueFromIdToken / getValueFromToken.
|
|
417
|
+
const { authenticated, getValueFromIdToken } = useTideCloak();
|
|
418
|
+
console.log('Auth:', authenticated, 'User:', getValueFromIdToken('preferred_username'));
|
|
419
|
+
// Should log: Auth: true, User: "<username>"
|
|
418
420
|
```
|
|
419
421
|
|
|
420
422
|
2. **Check provider wraps app**:
|
|
@@ -459,10 +461,10 @@ export async function GET() {
|
|
|
459
461
|
|
|
460
462
|
---
|
|
461
463
|
|
|
462
|
-
### ❌ Do Not
|
|
464
|
+
### ❌ Do Not Hand-Roll a Cookie-Presence Check in proxy.ts / middleware.ts
|
|
463
465
|
|
|
464
466
|
```typescript
|
|
465
|
-
// ❌ WRONG:
|
|
467
|
+
// ❌ WRONG: hand-written proxy/middleware that only checks a cookie exists
|
|
466
468
|
// proxy.ts (or middleware.ts in legacy projects)
|
|
467
469
|
import { NextResponse } from 'next/server';
|
|
468
470
|
|
|
@@ -474,14 +476,26 @@ export function middleware(req) {
|
|
|
474
476
|
}
|
|
475
477
|
```
|
|
476
478
|
|
|
477
|
-
**Why**:
|
|
478
|
-
1.
|
|
479
|
-
2.
|
|
479
|
+
**Why this hand-rolled version is wrong**:
|
|
480
|
+
1. Checks cookie presence, not JWT validity
|
|
481
|
+
2. No signature verification against the adapter's embedded `jwk`
|
|
480
482
|
3. Not real authorization
|
|
481
483
|
|
|
484
|
+
**Use the shipped helper instead.** `@tidecloak/nextjs/server` exports `createTideCloakProxy` (Next.js 16+, Node runtime) and `createTideCloakMiddleware` (Edge, legacy), which DO verify the token via `verifyTideCloakToken`:
|
|
485
|
+
```typescript
|
|
486
|
+
// proxy.ts (Next.js 16+) — createTideCloakMiddleware for middleware.ts on ≤15
|
|
487
|
+
import { createTideCloakProxy } from '@tidecloak/nextjs/server';
|
|
488
|
+
import tcConfig from './data/tidecloak.json';
|
|
489
|
+
|
|
490
|
+
export default createTideCloakProxy(tcConfig);
|
|
491
|
+
export const config = { matcher: ['/dashboard/:path*', '/admin/:path*'] };
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Even with a verifying proxy, keep server-side JWT verification inside API routes as the authoritative enforcement point (route interception governs page navigation, not direct API calls).
|
|
495
|
+
|
|
482
496
|
**Version rule**: Next.js 16+ uses `proxy.ts`. Next.js 15 and earlier use `middleware.ts`. Check the installed version before creating or looking for this file. See [canon/framework-matrix.md](../canon/framework-matrix.md#request-interception--route-protection-ui-only).
|
|
483
497
|
|
|
484
|
-
**Correct**: Client-side route guards for UX, server-side JWT verification for security.
|
|
498
|
+
**Correct**: Client-side route guards (or `createTideCloakProxy`) for UX, server-side JWT verification for security.
|
|
485
499
|
|
|
486
500
|
---
|
|
487
501
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Provision Hosted TideCloak via Skycloak
|
|
2
|
+
|
|
3
|
+
Provision a fully-managed TideCloak instance in Skycloak's cloud instead of self-hosting, then bootstrap the Tide realm on top of it. This is the hosted alternative to `deploy-tidecloak-docker`.
|
|
4
|
+
|
|
5
|
+
Read `canon/hosting-options.md` first — it covers the self-host vs hosted decision, the trust model, and the honest caveats you must surface to the operator.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- The team does not want to run auth infrastructure (containers, DB, upgrades, TLS, backups).
|
|
12
|
+
- You want a managed TideCloak reachable at a stable URL with no ops burden.
|
|
13
|
+
- You are prototyping and want an instance without local Docker.
|
|
14
|
+
|
|
15
|
+
**Do not use** if:
|
|
16
|
+
- The deployment must be air-gapped or fully self-controlled → `deploy-tidecloak-docker`.
|
|
17
|
+
- You already have a running TideCloak → go straight to realm bootstrap / integration.
|
|
18
|
+
- The operator has not confirmed a Skycloak account and API key exist (this playbook cannot create the account).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- A Skycloak account with a workspace (dashboard at `skycloak.io`).
|
|
25
|
+
- A Skycloak **API key** with scope `clusters:write` and `clusters:credentials:read`, created in Dashboard → Workspace → API keys. Shown once — capture it securely.
|
|
26
|
+
- `curl` and `jq`.
|
|
27
|
+
- Plan level sufficient for your region/size (non-US regions and larger sizes need Developer plan or higher; a `402 Payment Required` means the action isn't on the current plan).
|
|
28
|
+
|
|
29
|
+
**Secret handling (AP-HOST-3):** The API key and any cluster automation-client secret are operator/bootstrap secrets. Keep them in the shell/CI environment. Never write them into application code, the repo, or `tidecloak.json`. Same rule as master admin credentials (AP-41).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Overview of the flow
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
1. Create a TideCloak cluster (Skycloak API) → cluster id
|
|
37
|
+
2. Poll until status = available (Skycloak API) → cluster URL
|
|
38
|
+
3. Fetch automation-client creds (Skycloak API) → admin token source
|
|
39
|
+
4. Bootstrap the Tide realm (TideCloak admin API) → adapter JSON
|
|
40
|
+
5. Wire the app (unchanged) → same as self-hosted
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Steps 1–3 are Skycloak-specific and covered here. Step 4 is the **same Tide-realm bootstrap** as the self-host path — reuse `bootstrap-realm-from-template` / the `deploy-tidecloak-docker` init sequence, pointed at the hosted URL with a token from the automation client instead of master admin creds. Step 5 is identical to any Tide app (`add-auth-nextjs-fresh` etc.).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Step 1: Create a TideCloak cluster
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export SKYCLOAK_API_KEY="<your-key>"
|
|
51
|
+
API="https://api.skycloak.io"
|
|
52
|
+
VER="2026-06-01.beta"
|
|
53
|
+
|
|
54
|
+
# NOTE: exact request-body field names are INFERRED from the docs — if this 422s,
|
|
55
|
+
# inspect the RFC 9457 error body (it lists the offending field) and adjust.
|
|
56
|
+
curl -s -X POST "$API/clusters" \
|
|
57
|
+
-H "API-Key: $SKYCLOAK_API_KEY" \
|
|
58
|
+
-H "API-Version: $VER" \
|
|
59
|
+
-H "Content-Type: application/json" \
|
|
60
|
+
-d '{
|
|
61
|
+
"identityPlatform": "TideCloak",
|
|
62
|
+
"name": "myapp-auth",
|
|
63
|
+
"size": "Small",
|
|
64
|
+
"region": "us-east"
|
|
65
|
+
}' | tee cluster-create.json | jq .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **`identityPlatform: "TideCloak"`** is the field that makes this a Tide broker rather than plain Keycloak. Do not omit it.
|
|
69
|
+
- `size`: `Small` (DEV), `Medium` (STAGING), `Large` (PROD).
|
|
70
|
+
- Capture the returned cluster **id**: `CLUSTER_ID=$(jq -r '.id' cluster-create.json)`.
|
|
71
|
+
- Creation is **asynchronous** — the response comes back before the cluster is ready.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Step 2: Poll until available
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
for i in $(seq 1 40); do
|
|
79
|
+
STATUS=$(curl -s "$API/clusters/$CLUSTER_ID" \
|
|
80
|
+
-H "API-Key: $SKYCLOAK_API_KEY" -H "API-Version: $VER" | jq -r '.status')
|
|
81
|
+
echo "attempt $i: $STATUS"
|
|
82
|
+
case "$STATUS" in
|
|
83
|
+
available) echo "ready"; break ;;
|
|
84
|
+
failed) echo "provisioning FAILED — check the Skycloak dashboard"; exit 1 ;;
|
|
85
|
+
esac
|
|
86
|
+
sleep 15
|
|
87
|
+
done
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Provisioning is typically 2–4 minutes; Skycloak also emails on completion. Do not proceed to bootstrap until status is `available`. The cluster is reachable at `https://<cluster-id>.app.skycloak.io`:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
TIDECLOAK_URL="https://${CLUSTER_ID}.app.skycloak.io"
|
|
94
|
+
curl -s -f "$TIDECLOAK_URL" > /dev/null && echo "TideCloak reachable" || echo "not reachable yet"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Step 3: Get an admin token (no master password exists)
|
|
100
|
+
|
|
101
|
+
Skycloak does **not** issue a Keycloak admin username/password. Two ways to reach the admin API:
|
|
102
|
+
|
|
103
|
+
**A. Admin Console SSO (interactive)** — for a human clicking through: open the cluster's "Go to Console" from the Skycloak dashboard, authenticated by your Skycloak account. Use this to eyeball the instance; it does not give a scriptable token.
|
|
104
|
+
|
|
105
|
+
**B. Automation client (scriptable)** — each cluster has a confidential OAuth2 client `skycloak-automation-<cluster-id>` in the `master` realm for programmatic admin access. Fetch its credentials, then use client-credentials to mint admin tokens:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Field names of the credentials response are INFERRED — inspect the actual JSON.
|
|
109
|
+
curl -s "$API/clusters/$CLUSTER_ID/credentials" \
|
|
110
|
+
-H "API-Key: $SKYCLOAK_API_KEY" -H "API-Version: $VER" | tee cluster-creds.json | jq .
|
|
111
|
+
|
|
112
|
+
CLIENT_ID=$(jq -r '.clientId // .automationClientId // empty' cluster-creds.json)
|
|
113
|
+
CLIENT_SECRET=$(jq -r '.clientSecret // .secret // empty' cluster-creds.json)
|
|
114
|
+
|
|
115
|
+
# Mint an admin token against the hosted instance's master realm:
|
|
116
|
+
get_token() {
|
|
117
|
+
curl -s -X POST "$TIDECLOAK_URL/realms/master/protocol/openid-connect/token" \
|
|
118
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
119
|
+
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" \
|
|
120
|
+
| jq -r '.access_token'
|
|
121
|
+
}
|
|
122
|
+
TOKEN="$(get_token)"
|
|
123
|
+
[ -n "$TOKEN" ] && [ "$TOKEN" != "null" ] && echo "admin token OK" || echo "token failed — check creds/scopes"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This `get_token` replaces the master-admin `get_token` in the self-host init script. Everything downstream (realm create, `setUpTideRealm`, IGA toggle, change-request authorize/commit, adapter export) is the same — just pointed at `$TIDECLOAK_URL` with this token.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Step 4: Bootstrap the Tide realm
|
|
131
|
+
|
|
132
|
+
Run the standard Tide-realm bootstrap against the hosted instance:
|
|
133
|
+
|
|
134
|
+
1. `bootstrap-realm-from-template` — create the realm from the `realm.json` template (identical template to self-host; see `deploy-tidecloak-docker` Step 2), then `setUpTideRealm` + enable IGA.
|
|
135
|
+
2. `initialize-admin-and-link-account` — create the admin user, assign `tide-realm-admin`, generate the Tide-account link, approve the change requests, export the adapter JSON.
|
|
136
|
+
|
|
137
|
+
Use the `get_token()` from Step 3 (automation client) everywhere the self-host script uses the master-admin token. Point `TIDECLOAK_URL` at `https://<cluster-id>.app.skycloak.io`.
|
|
138
|
+
|
|
139
|
+
> **VERIFY THIS ON THE LIVE INSTANCE (GAP-066).** The pack cannot yet confirm that a Skycloak-hosted TideCloak exposes the full Tide vendor surface (`setUpTideRealm`, `toggle-iga`, the change-request API, adapter export with `jwk`/`vendorId`/`homeOrkUrl`) or how Tide **licensing** is handled in the hosted context. Before promising a turnkey result:
|
|
140
|
+
> - Probe `POST .../vendorResources/setUpTideRealm` and check for a 2xx + licensing JSON.
|
|
141
|
+
> - After bootstrap, confirm the exported adapter JSON contains `jwk`, `vendorId`, `homeOrkUrl` (I-05, I-13).
|
|
142
|
+
> - If those endpoints 404 or the adapter lacks Tide fields, **stop** — Skycloak is hosting the broker but Tide-realm provisioning/licensing needs the partner's Tide-specific path. Report this to the operator and raise with the Tide/Skycloak teams. Do not fabricate the adapter JSON.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Step 5: Wire the app
|
|
147
|
+
|
|
148
|
+
Identical to any Tide app — the hosting choice does not change integration. Adapter JSON goes to `data/tidecloak.json` (Next.js) or `public/tidecloak.json` (React/Vite), with `auth-server-url` pointing at the hosted URL. Continue with `add-auth-nextjs-fresh` / `add-auth-nextjs-existing`, then route/API protection.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Verification Checklist
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Cluster available
|
|
156
|
+
curl -s "$API/clusters/$CLUSTER_ID" -H "API-Key: $SKYCLOAK_API_KEY" -H "API-Version: $VER" | jq '.status'
|
|
157
|
+
# → "available"
|
|
158
|
+
|
|
159
|
+
# Hosted TideCloak reachable
|
|
160
|
+
curl -s -f "https://${CLUSTER_ID}.app.skycloak.io" > /dev/null && echo reachable
|
|
161
|
+
|
|
162
|
+
# Admin token from automation client works
|
|
163
|
+
[ -n "$(get_token)" ] && echo "token OK"
|
|
164
|
+
|
|
165
|
+
# After bootstrap: adapter has Tide extensions (the real turnkey test — GAP-066)
|
|
166
|
+
jq 'has("jwk") and has("vendorId") and has("homeOrkUrl")' data/tidecloak.json
|
|
167
|
+
# → true (if false: GAP-066 — hosted Tide vendor surface not confirmed)
|
|
168
|
+
|
|
169
|
+
# IGA enabled on the hosted realm
|
|
170
|
+
curl -s "https://${CLUSTER_ID}.app.skycloak.io/admin/realms/myapp" \
|
|
171
|
+
-H "Authorization: Bearer $(get_token)" | jq '.attributes.isIGAEnabled'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Common Failures
|
|
177
|
+
|
|
178
|
+
### `402 Payment Required` on create
|
|
179
|
+
The chosen region/size isn't available on the current plan (e.g. a non-US region on a trial workspace). Use a US region / `Small` size, or upgrade the plan. The RFC 9457 body's `detail` states the constraint.
|
|
180
|
+
|
|
181
|
+
### `403` "does not have the required scope"
|
|
182
|
+
The API key lacks `clusters:write` (create) or `clusters:credentials:read` (Step 3). Create a key with the right scopes; note write includes read.
|
|
183
|
+
|
|
184
|
+
### Missing `API-Version` header (AP-HOST-4)
|
|
185
|
+
Every Skycloak call needs `-H "API-Version: 2026-06-01.beta"`. Omitting it fails the request.
|
|
186
|
+
|
|
187
|
+
### `422` on create with a field complaint
|
|
188
|
+
The request-body field names here are INFERRED from the docs. Read the `errors[]` array in the RFC 9457 response — it names the offending `field` — and adjust (`identityPlatform`/`size`/`region` spellings, enum values).
|
|
189
|
+
|
|
190
|
+
### Cluster stuck in `provisioning` / went `failed`
|
|
191
|
+
Provisioning is async and occasionally fails server-side. Check the Skycloak dashboard for the cluster's error detail; recreate if `failed`. Do not bootstrap against a non-`available` cluster.
|
|
192
|
+
|
|
193
|
+
### Adapter JSON missing Tide fields after bootstrap (GAP-066)
|
|
194
|
+
The hosted instance may not expose the Tide vendor surface, or licensing wasn't provisioned. Do not fall back to `createRemoteJWKSet` or hand-build the adapter (I-04). Stop and escalate — this is the open question the hosted path depends on.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Anti-Patterns
|
|
199
|
+
|
|
200
|
+
- **AP-HOST-2** — Telling the user the hosted Tide path is fully turnkey before GAP-066 is confirmed. Cluster provisioning is verified; the Tide-realm bootstrap on top is not.
|
|
201
|
+
- **AP-HOST-3** — Putting `SKYCLOAK_API_KEY` or the `skycloak-automation-*` secret in app code, `tidecloak.json`, or the repo. Bootstrap secrets only.
|
|
202
|
+
- **AP-HOST-4** — Omitting the `API-Version` header.
|
|
203
|
+
- **Do not** fabricate `tidecloak.json` if the hosted vendor endpoints are absent. A missing adapter is a provisioning problem to escalate, not a file to invent (I-05, I-13).
|
|
204
|
+
- **Do not** present hosting as either a security downgrade or a magic upgrade — state the real trust model from `canon/hosting-options.md` (availability + metadata + Tideless-IGA caveat).
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## References
|
|
209
|
+
|
|
210
|
+
- `canon/hosting-options.md` — decision, trust model, Skycloak API reference, GAP-066
|
|
211
|
+
- Skycloak docs: `https://skycloak.io/docs/api/` (API-Version `2026-06-01.beta`)
|
|
212
|
+
- `deploy-tidecloak-docker` — the self-host equivalent and the shared realm-bootstrap sequence
|
|
213
|
+
- `bootstrap-realm-from-template`, `initialize-admin-and-link-account` — the Tide-realm steps reused in Step 4
|
|
@@ -289,24 +289,27 @@ const executeTideRequest = async (request, waitForAll = false) => {
|
|
|
289
289
|
WRONG: GET /admin/realms/{realm}/tide-admin/realm-policy
|
|
290
290
|
-> { status: "none" } (even when the policy exists)
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
WRONG: GET /realms/{realm}/tide-policy-resources/admin-policy
|
|
293
|
+
-> does NOT exist on current main (the public policy endpoint was removed)
|
|
294
|
+
|
|
295
|
+
CORRECT: GET /admin/realms/{realm}/iga/role-policies (admin bearer required)
|
|
296
|
+
-> JSON; the signed admin policy bytes are in the `policy` field
|
|
294
297
|
```
|
|
295
298
|
|
|
296
|
-
The correct endpoint
|
|
299
|
+
The correct endpoint is an admin API route and **requires an admin bearer token**. Proxy through your backend so the browser never holds the admin credential.
|
|
297
300
|
|
|
298
|
-
**CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins. Direct browser fetch returns an opaque/blocked response. Create a same-origin API route that proxies the request server-side. VERIFIED (session-001).
|
|
301
|
+
**CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins, and this is a privileged admin API. Direct browser fetch returns an opaque/blocked response (and would leak the admin bearer). Create a same-origin API route that proxies the request server-side with the admin token. VERIFIED (session-001).
|
|
299
302
|
|
|
300
303
|
**If the proxy route is protected with `withAuth` (correct per I-03), the client must use authenticated fetch** (e.g., `appFetch` or `secureFetch` with Bearer header). A bare `fetch()` without Authorization returns 401. The resulting empty/error response produces undefined policy bytes, causing `PolicyAuthorizationFlowException: "Model does not have a policy passed with it"` at signing time. VERIFIED (LEARNINGS-batch-009 L-08).
|
|
301
304
|
|
|
302
305
|
**There is no REST API for deploying Forseti contracts.** Do not try `PUT` or `POST` to `/tide-admin/forseti-contracts` — these endpoints do not exist on the dev image (404/405). Contract deployment happens via `PolicySignRequest.addForsetiContractToUpload(contractSource)` in the browser signing flow (Step 4). VERIFIED (session-001).
|
|
303
306
|
|
|
304
|
-
**The
|
|
307
|
+
**The signed bytes arrive as a base64 string in the `policy` field, not raw binary.** Read the `policy` field and decode before using:
|
|
305
308
|
|
|
306
309
|
```js
|
|
307
|
-
// Backend proxy
|
|
308
|
-
const
|
|
309
|
-
const raw = Buffer.from(
|
|
310
|
+
// Backend proxy (holds the admin bearer)
|
|
311
|
+
const json = await tcRes.json();
|
|
312
|
+
const raw = Buffer.from(json.policy, "base64");
|
|
310
313
|
const bytes = Array.from(new Uint8Array(raw));
|
|
311
314
|
res.json({ policyBytes: bytes });
|
|
312
315
|
|
|
@@ -315,12 +318,12 @@ const data = await res.json();
|
|
|
315
318
|
const adminPolicyBytes = new Uint8Array(data.policyBytes);
|
|
316
319
|
```
|
|
317
320
|
|
|
318
|
-
**Common mistake:** Treating the base64 string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
|
|
321
|
+
**Common mistake:** Treating the base64 `policy` string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
|
|
319
322
|
|
|
320
|
-
**Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
|
|
323
|
+
**Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//admin/realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
|
|
321
324
|
```js
|
|
322
325
|
const baseUrl = config["auth-server-url"].replace(/\/+$/, "");
|
|
323
|
-
const adminPolicyUrl = `${baseUrl}/realms/${config.realm}/
|
|
326
|
+
const adminPolicyUrl = `${baseUrl}/admin/realms/${config.realm}/iga/role-policies`;
|
|
324
327
|
```
|
|
325
328
|
|
|
326
329
|
---
|
|
@@ -528,50 +531,29 @@ These are prep steps. None produce VVK-signed bytes.
|
|
|
528
531
|
|
|
529
532
|
Role assignments that produce VVK-signed UserContexts MUST be manually approved through the approval enclave in the admin's browser. You cannot automate this from a backend script.
|
|
530
533
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
```
|
|
534
|
-
POST /tide-admin/change-set/sign
|
|
535
|
-
Response: { "requiresApprovalPopup": true, "changeSetDraftRequests": "base64..." }
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
The `requiresApprovalPopup: true` response means the backend CANNOT complete the signing. The VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles. This signing must happen through the ORK threshold network via the Tide enclave running in the admin's browser.
|
|
539
|
-
|
|
540
|
-
**Always use `/batch` endpoints for approval:**
|
|
534
|
+
This is Tide MultiAdmin mode: the VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles, and the signing must happen through the ORK threshold network via the Tide enclave in the admin's browser. The backend CANNOT complete it — a bare `authorize` on such a CR requires the two-phase `approval-model` enclave exchange. (Endpoints: current `/iga/change-requests/...` surface — see `canon/iga-change-requests-api.md`.)
|
|
541
535
|
|
|
542
536
|
```js
|
|
543
|
-
// 1. Backend
|
|
537
|
+
// 1. Backend performs the governed change (role assignment) → 202, a CR is created
|
|
544
538
|
POST /admin/realms/{realm}/users/{userId}/role-mappings/realm
|
|
545
539
|
|
|
546
|
-
// 2. Frontend fetches pending
|
|
547
|
-
GET /admin/realms/{realm}/
|
|
540
|
+
// 2. Frontend fetches pending CRs
|
|
541
|
+
GET /admin/realms/{realm}/iga/change-requests?status=PENDING // objects keyed by `id`
|
|
548
542
|
|
|
549
|
-
// 3.
|
|
550
|
-
|
|
551
|
-
|
|
543
|
+
// 3. For each CR, fetch the enclave challenge (Tide MultiAdmin)
|
|
544
|
+
GET /admin/realms/{realm}/iga/change-requests/{id}/approval-model
|
|
545
|
+
// → { changeRequestId, requestModel: "base64..." }
|
|
552
546
|
|
|
553
|
-
// 4. Collect
|
|
554
|
-
const enclaveRequests =
|
|
555
|
-
.filter(r => r.requiresApprovalPopup)
|
|
556
|
-
.map(r => ({ id: r.changesetId, request: base64ToBytes(r.changeSetDraftRequests) }));
|
|
547
|
+
// 4. Collect the challenges, pass to ONE enclave approval popup
|
|
548
|
+
const enclaveRequests = pending.map(cr => ({ id: cr.id, request: base64ToBytes(cr.requestModel) }));
|
|
557
549
|
const approvals = await tc.requestTideOperatorApproval(enclaveRequests);
|
|
558
550
|
|
|
559
|
-
// 5. Submit each signed
|
|
560
|
-
POST /admin/realms/{realm}/
|
|
561
|
-
|
|
562
|
-
// 6. Commit all in batch
|
|
563
|
-
POST /admin/realms/{realm}/tide-admin/change-set/commit/batch
|
|
564
|
-
{ changeSets: [{ changeSetId, changeSetType, actionType }] }
|
|
565
|
-
```
|
|
551
|
+
// 5. Submit each signed doken back to the same endpoint
|
|
552
|
+
POST /admin/realms/{realm}/iga/change-requests/{id}/approval-model
|
|
553
|
+
{ "requestModel": "<base64 doken>" } // → { recorded, authCount, threshold }
|
|
566
554
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
```js
|
|
570
|
-
const formData = new FormData();
|
|
571
|
-
formData.append("changeSetId", id);
|
|
572
|
-
formData.append("actionType", actionType);
|
|
573
|
-
formData.append("changeSetType", changeSetType);
|
|
574
|
-
formData.append("requests", btoa(String.fromCharCode(...signedBytes)));
|
|
555
|
+
// 6. Commit each CR once ready (readyToCommit === true)
|
|
556
|
+
POST /admin/realms/{realm}/iga/change-requests/{id}/commit // 412 if still under threshold
|
|
575
557
|
```
|
|
576
558
|
|
|
577
559
|
After commit: the user must refresh their token (or re-login) for the new role to appear in their doken. Use `IAMService.forceUpdateToken()` or `useTideCloak().forceRefreshToken()`.
|
|
@@ -656,8 +638,8 @@ The forseti-crypto-quickstart is the reference implementation that works against
|
|
|
656
638
|
- [ ] `modelId` is `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]` (array, not string)
|
|
657
639
|
- [ ] `PolicySignRequest` imported from `heimdall-tide`
|
|
658
640
|
- [ ] `BaseTideRequest.decode()` called on `createTideRequest` result before approval
|
|
659
|
-
- [ ] Admin policy fetched from `GET /realms/{realm}/tide-policy-resources/admin-policy`
|
|
660
|
-
- [ ] Admin policy bytes are base64-decoded (not raw char codes)
|
|
641
|
+
- [ ] Admin policy fetched (admin bearer) from `GET /admin/realms/{realm}/iga/role-policies`; signed bytes read from the `policy` field (the public `tide-policy-resources/admin-policy` endpoint does not exist on main)
|
|
642
|
+
- [ ] Admin policy bytes are base64-decoded from the `policy` field (not raw char codes)
|
|
661
643
|
- [ ] Approval popup opens and admin approves
|
|
662
644
|
- [ ] `executeSignRequest` called with `waitForAll = true`
|
|
663
645
|
- [ ] `policy.signature = signatures[0]` assigned before `policy.toBytes()`
|
|
@@ -701,8 +683,8 @@ The forseti-crypto-quickstart is the reference implementation that works against
|
|
|
701
683
|
|
|
702
684
|
### `{ status: "none" }` when fetching admin policy
|
|
703
685
|
|
|
704
|
-
**Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`.
|
|
705
|
-
**Fix:** Use `GET /realms/{realm}/
|
|
686
|
+
**Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`. (The old public `GET /realms/{realm}/tide-policy-resources/admin-policy` endpoint no longer exists on main either.)
|
|
687
|
+
**Fix:** Use `GET /admin/realms/{realm}/iga/role-policies` with an admin bearer token and read the signed bytes from the `policy` field.
|
|
706
688
|
|
|
707
689
|
### Admin policy bytes start with `[65, 81, 65, 65, ...]`
|
|
708
690
|
|