@tideorg/mcp 1.4.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/GAP_REGISTER.md +117 -0
- package/README.md +38 -0
- package/adapters/AGENTS.md +241 -0
- package/adapters/CLAUDE.md +146 -0
- package/adapters/replit.md +224 -0
- package/canon/anti-patterns.md +2133 -0
- package/canon/concepts.md +816 -0
- package/canon/custom-contracts.md +533 -0
- package/canon/delegation.md +195 -0
- package/canon/feature-mapping.md +637 -0
- package/canon/framework-matrix.md +1125 -0
- package/canon/invariants.md +851 -0
- package/canon/redirect-handler.md +254 -0
- package/canon/tidecloak-bootstrap.md +287 -0
- package/canon/tidecloak-endpoints.md +294 -0
- package/canon/troubleshooting.md +1494 -0
- package/canon/version-policy.md +89 -0
- package/mcp-server/dist/index.d.ts +2 -0
- package/mcp-server/dist/index.js +605 -0
- package/package.json +45 -0
- package/playbooks/add-auth-nextjs-existing.md +799 -0
- package/playbooks/add-auth-nextjs-fresh.md +654 -0
- package/playbooks/add-rbac-nextjs.md +190 -0
- package/playbooks/bootstrap-realm-from-template.md +170 -0
- package/playbooks/configure-e2ee-roles-and-policies.md +196 -0
- package/playbooks/deploy-tidecloak-docker.md +792 -0
- package/playbooks/diagnose-broken-login.md +234 -0
- package/playbooks/diagnose-missing-roles-or-claims.md +253 -0
- package/playbooks/initialize-admin-and-link-account.md +235 -0
- package/playbooks/migrate-from-existing-auth.md +198 -0
- package/playbooks/protect-api-nextjs.md +451 -0
- package/playbooks/protect-routes-nextjs.md +544 -0
- package/playbooks/setup-forseti-e2ee.md +756 -0
- package/playbooks/setup-iga-admin-panel.md +344 -0
- package/playbooks/setup-server-delegation.md +142 -0
- package/playbooks/start-tidecloak-dev.md +130 -0
- package/playbooks/verify-jwt-server-side.md +439 -0
- package/prompts/add-admin-approval-flow.md +50 -0
- package/prompts/build-private-customer-portal.md +85 -0
- package/prompts/migrate-generic-auth-to-tide.md +67 -0
- package/prompts/secure-existing-app.md +80 -0
- package/reference-apps/INDEX.md +87 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +123 -0
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +152 -0
- package/reference-apps/encrypted-communication/manifest.yaml +100 -0
- package/reference-apps/encrypted-communication/role-policy-matrix.md +80 -0
- package/reference-apps/encrypted-communication/scenario.md +134 -0
- package/reference-apps/git-pr-signing-service/anti-patterns.md +61 -0
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +157 -0
- package/reference-apps/git-pr-signing-service/manifest.yaml +80 -0
- package/reference-apps/git-pr-signing-service/role-policy-matrix.md +99 -0
- package/reference-apps/git-pr-signing-service/scenario.md +169 -0
- package/reference-apps/iga-admin-governance/anti-patterns.md +133 -0
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +153 -0
- package/reference-apps/iga-admin-governance/manifest.yaml +87 -0
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +67 -0
- package/reference-apps/iga-admin-governance/scenario.md +126 -0
- package/reference-apps/organisation-password-manager/anti-patterns.md +71 -0
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +127 -0
- package/reference-apps/organisation-password-manager/manifest.yaml +86 -0
- package/reference-apps/organisation-password-manager/role-policy-matrix.md +59 -0
- package/reference-apps/organisation-password-manager/scenario.md +107 -0
- package/reference-apps/policy-governed-signing/anti-patterns.md +67 -0
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +128 -0
- package/reference-apps/policy-governed-signing/manifest.yaml +78 -0
- package/reference-apps/policy-governed-signing/role-policy-matrix.md +62 -0
- package/reference-apps/policy-governed-signing/scenario.md +113 -0
- package/skills/tide-diagnostics/SKILL.md +99 -0
- package/skills/tide-integration/SKILL.md +136 -0
- package/skills/tide-learning-capture/SKILL.md +174 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +214 -0
- package/skills/tide-reviewer/SKILL.md +133 -0
- package/skills/tide-route-and-api-protection/SKILL.md +201 -0
- package/skills/tide-scenario-resolver/SKILL.md +81 -0
- package/skills/tide-setup/SKILL.md +218 -0
- package/skills/tide-solutions-architect/SKILL.md +130 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# Custom Forseti Contracts - Canon
|
|
2
|
+
|
|
3
|
+
How to write, deploy, and use custom Forseti smart contracts for access control.
|
|
4
|
+
|
|
5
|
+
Forseti contracts are C# code that runs in a sandboxed VM on every ORK node. They control who can sign, encrypt, decrypt, or approve operations. No single ORK can bypass a contract - majority must agree.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What You Can Build
|
|
10
|
+
|
|
11
|
+
A contract is arbitrary C# logic that every ORK evaluates independently before allowing a cryptographic operation. A majority of ORKs must agree. No single server, admin, or compromised node can bypass it.
|
|
12
|
+
|
|
13
|
+
This means you can encode any business rule directly into the cryptographic layer:
|
|
14
|
+
|
|
15
|
+
**Agent guidance**: When a user describes what their app needs to protect, control, or enforce - think in contracts. Any business rule that should be tamper-proof can become a contract. The user does not need to know they need a contract. They say "only managers should approve expenses over $5K" or "patients should control who sees their records" or "we need three people to sign off on a deployment." Your job is to recognize that these are contract problems and write the C# that enforces them.
|
|
16
|
+
|
|
17
|
+
Be creative. The contract API gives you roles, identity, time, geo, payload data, multi-party approval, and separation of duties. Combine them. A user who says "build me a password manager" gets self-encryption. A user who says "build me a password manager with recovery" gets a contract that requires 3-of-5 recovery contacts. A user who says "sign transactions" gets a contract that checks amounts against limits and requires more approvers for larger values. A user who says "encrypt medical records" gets a contract that checks the accessor's role, the time of day, and logs every access attempt.
|
|
18
|
+
|
|
19
|
+
The contract runs on every ORK independently. No server, admin, or single point of compromise can bypass it. This is the differentiator. When the user's requirement is "nobody should be able to cheat this rule" - that is a contract.
|
|
20
|
+
|
|
21
|
+
Do not wait for the user to ask for a Forseti contract. Recognize when their requirements imply one and propose it.
|
|
22
|
+
|
|
23
|
+
You can write any C# logic that fits within the sandbox constraints. There are no pre-built templates you must follow. The SDK gives you building blocks - role checks, time checks, doken inspection, payload parsing, approval counting - but the contract itself is freeform C# code. If the user needs a rule that does not map to any existing pattern, write new logic. Parse the payload bytes. Compare values. Count things. Check conditions. Combine checks in ways nobody has combined them before. The contract is your code, meeting the user's specific need, enforced by the network. Write exactly what the situation requires.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## When You Need a Custom Contract
|
|
28
|
+
|
|
29
|
+
Use a built-in contract for simple role-gated multi-approval. Write a custom contract when you need:
|
|
30
|
+
|
|
31
|
+
- Executor validation (who can perform the action)
|
|
32
|
+
- Data validation (what data is being signed/encrypted)
|
|
33
|
+
- Time-based restrictions (business hours, weekdays)
|
|
34
|
+
- Complex approval logic (minimum from different orgs, no self-approval)
|
|
35
|
+
- Direction-specific logic (different rules for encrypt vs decrypt)
|
|
36
|
+
- Amount/threshold checks on payload data
|
|
37
|
+
- Separation of duties enforcement
|
|
38
|
+
- Multi-jurisdictional approval requirements
|
|
39
|
+
|
|
40
|
+
### Built-in Contracts
|
|
41
|
+
|
|
42
|
+
| Contract ID | Checks | Parameters |
|
|
43
|
+
|-------------|--------|------------|
|
|
44
|
+
| `GenericResourceAccessThresholdRole:1` | Approvers have a **client role** | `role`, `resource`, `threshold` |
|
|
45
|
+
| `GenericRealmAccessThresholdRole:1` | Approvers have a **realm role** | `role`, `resource`, `threshold` |
|
|
46
|
+
| `SimpleTagBasedDecryption:1` | Executor has `_tide_{tag}.encrypt`/`.decrypt` role | tag from data |
|
|
47
|
+
|
|
48
|
+
Built-in contracts only validate approvers (or tags). They do NOT validate the executor or the data. For anything beyond "N people with role X must approve", write a custom contract.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Contract Template
|
|
53
|
+
|
|
54
|
+
Every contract must:
|
|
55
|
+
- Use namespace `Ork.Forseti.Sdk`
|
|
56
|
+
- Have a class named `Contract`
|
|
57
|
+
- Implement `IAccessPolicy`
|
|
58
|
+
|
|
59
|
+
```csharp
|
|
60
|
+
using Ork.Forseti.Sdk;
|
|
61
|
+
|
|
62
|
+
public class Contract : IAccessPolicy
|
|
63
|
+
{
|
|
64
|
+
// Parameters bound automatically from policy config
|
|
65
|
+
[PolicyParam(Required = true, Description = "Role required to execute")]
|
|
66
|
+
public string Role { get; set; }
|
|
67
|
+
|
|
68
|
+
[PolicyParam(Required = true, Description = "Resource/client to check role against")]
|
|
69
|
+
public string Resource { get; set; }
|
|
70
|
+
|
|
71
|
+
// STEP 1: Validate request data (always runs)
|
|
72
|
+
public PolicyDecision ValidateData(DataContext ctx)
|
|
73
|
+
{
|
|
74
|
+
return PolicyDecision.Allow();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// STEP 2: Validate approvers (runs if ApprovalType.EXPLICIT)
|
|
78
|
+
public PolicyDecision ValidateApprovers(ApproversContext ctx)
|
|
79
|
+
{
|
|
80
|
+
return PolicyDecision.Allow();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// STEP 3: Validate executor (runs if ExecutionType.PRIVATE)
|
|
84
|
+
public PolicyDecision ValidateExecutor(ExecutorContext ctx)
|
|
85
|
+
{
|
|
86
|
+
var executor = new DokenDto(ctx.Doken);
|
|
87
|
+
|
|
88
|
+
return Decision
|
|
89
|
+
.RequireNotExpired(executor)
|
|
90
|
+
.RequireRole(executor, Resource, Role);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Three Validation Methods
|
|
98
|
+
|
|
99
|
+
| Method | When it runs | Context | Use for |
|
|
100
|
+
|--------|-------------|---------|---------|
|
|
101
|
+
| `ValidateData(DataContext ctx)` | Always | `ctx.Data`, `ctx.DynamicData`, `ctx.RequestId` | Time checks, data format validation, direction detection |
|
|
102
|
+
| `ValidateApprovers(ApproversContext ctx)` | When `ApprovalType.EXPLICIT` | `ctx.Dokens` (list of approver tokens) | Quorum checks, role-based approval, org diversity |
|
|
103
|
+
| `ValidateExecutor(ExecutorContext ctx)` | When `ExecutionType.PRIVATE` | `ctx.Doken` (executor's token) | Role checks, identity checks, expiry |
|
|
104
|
+
|
|
105
|
+
Each returns `PolicyDecision.Allow()` or `PolicyDecision.Deny("reason")`.
|
|
106
|
+
|
|
107
|
+
### Direction Detection (Encrypt vs Decrypt)
|
|
108
|
+
|
|
109
|
+
For encryption contracts, detect the direction via `ctx.RequestId`:
|
|
110
|
+
|
|
111
|
+
```csharp
|
|
112
|
+
public PolicyDecision ValidateData(DataContext ctx)
|
|
113
|
+
{
|
|
114
|
+
if (ctx.RequestId == "PolicyEnabledEncryption:1")
|
|
115
|
+
{
|
|
116
|
+
// Encrypt-specific validation
|
|
117
|
+
return PolicyDecision.Allow();
|
|
118
|
+
}
|
|
119
|
+
if (ctx.RequestId == "PolicyEnabledDecryption:1")
|
|
120
|
+
{
|
|
121
|
+
// Decrypt-specific validation
|
|
122
|
+
return Decision.RequireWeekday();
|
|
123
|
+
}
|
|
124
|
+
return PolicyDecision.Deny("Unknown request type");
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Decision Builder
|
|
131
|
+
|
|
132
|
+
Chain checks with `Decision.`. First failure stops the chain and returns the deny reason.
|
|
133
|
+
|
|
134
|
+
### Role Checks
|
|
135
|
+
|
|
136
|
+
```csharp
|
|
137
|
+
Decision
|
|
138
|
+
.RequireRole(doken, "resource", "role") // client role
|
|
139
|
+
.RequireRole(doken, "admin") // realm role (2-arg)
|
|
140
|
+
.RequireAnyRole(doken, "resource", "admin", "mod") // at least one
|
|
141
|
+
.RequireAllRoles(doken, "resource", "read", "write") // must have all
|
|
142
|
+
.ForbidRole(doken, "resource", "blocked") // must NOT have
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Approval Checks
|
|
146
|
+
|
|
147
|
+
```csharp
|
|
148
|
+
Decision
|
|
149
|
+
.RequireMinWithRole(approvers, 2, "resource", "approver") // N approvers with role
|
|
150
|
+
.RequireAnyWithRole(approvers, "resource", "approver") // at least one
|
|
151
|
+
.ForbidSelfApproval(requestorId, approvers) // no self-approval
|
|
152
|
+
.RequireDistinctOrgs(approvers, 2) // from N different orgs
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Time Checks
|
|
156
|
+
|
|
157
|
+
```csharp
|
|
158
|
+
Decision
|
|
159
|
+
.RequireWeekday() // Mon-Fri
|
|
160
|
+
.RequireBusinessHours() // Mon-Fri 9-17 UTC
|
|
161
|
+
.RequireHourBetween(9, 17) // custom hours
|
|
162
|
+
.ForbidHourBetween(0, 6) // block hours
|
|
163
|
+
.RequireDayOfWeek(DayOfWeek.Monday) // specific day
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Token Checks
|
|
167
|
+
|
|
168
|
+
```csharp
|
|
169
|
+
Decision
|
|
170
|
+
.RequireNotExpired(doken)
|
|
171
|
+
.RequireFromAudience(doken, "my-realm")
|
|
172
|
+
.RequireUserId(doken, "user-123")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Geo Checks
|
|
176
|
+
|
|
177
|
+
```csharp
|
|
178
|
+
var country = ForsetiSdk.Claim("country") as string;
|
|
179
|
+
Decision
|
|
180
|
+
.RequireCountry(country, "US", "CA", "AU")
|
|
181
|
+
.ForbidCountry(country, "XX", "YY")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Generic
|
|
185
|
+
|
|
186
|
+
```csharp
|
|
187
|
+
Decision
|
|
188
|
+
.Require(amount <= maxAmount, $"Amount {amount} exceeds limit {maxAmount}")
|
|
189
|
+
.Forbid(isBlacklisted, "User is blacklisted")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## DokenDto - Token Wrapper
|
|
195
|
+
|
|
196
|
+
Wrap doken bytes to access user identity, roles, and expiry.
|
|
197
|
+
|
|
198
|
+
```csharp
|
|
199
|
+
// Single executor
|
|
200
|
+
var executor = new DokenDto(ctx.Doken);
|
|
201
|
+
|
|
202
|
+
// List of approvers
|
|
203
|
+
var approvers = DokenDto.WrapAll(ctx.Dokens);
|
|
204
|
+
|
|
205
|
+
// Properties
|
|
206
|
+
executor.UserId // user ID string
|
|
207
|
+
executor.Audience // realm/org
|
|
208
|
+
executor.Expiry // unix timestamp
|
|
209
|
+
executor.IsExpired // bool
|
|
210
|
+
executor.IsNull // bool
|
|
211
|
+
executor.HasRole("resource", "admin") // client role
|
|
212
|
+
executor.HasAnyRole("resource", "admin", "mod")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## [PolicyParam] - Declare Parameters
|
|
218
|
+
|
|
219
|
+
Parameters are bound automatically from the policy configuration. Declare them as properties with `[PolicyParam]`.
|
|
220
|
+
|
|
221
|
+
```csharp
|
|
222
|
+
[PolicyParam(Required = true, Description = "Role name")]
|
|
223
|
+
public string Role { get; set; }
|
|
224
|
+
|
|
225
|
+
[PolicyParam(Default = 2, Min = 1, Max = 10)]
|
|
226
|
+
public int MinApprovers { get; set; }
|
|
227
|
+
|
|
228
|
+
[PolicyParam(AllowedValues = new[] { "low", "medium", "high" })]
|
|
229
|
+
public string Priority { get; set; } = "medium";
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| Option | Description |
|
|
233
|
+
|--------|-------------|
|
|
234
|
+
| `Required` | Throws if missing (default: false) |
|
|
235
|
+
| `Default` | Default value if not provided |
|
|
236
|
+
| `Min` / `Max` | Numeric range validation |
|
|
237
|
+
| `AllowedValues` | Enum-like string validation |
|
|
238
|
+
| `Description` | Human-readable description |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## ForsetiSdk Runtime
|
|
243
|
+
|
|
244
|
+
Available inside contract methods:
|
|
245
|
+
|
|
246
|
+
```csharp
|
|
247
|
+
ForsetiSdk.Claim("key") // get a claim value (costs 5 gas)
|
|
248
|
+
ForsetiSdk.Log("message") // log output (costs 25 gas)
|
|
249
|
+
ForsetiSdk.GasUsed // current gas consumed
|
|
250
|
+
ForsetiSdk.GasLimit // total gas budget (default 50,000)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Gas limit is 50,000. Exceeding it throws `OutOfGasException` and the operation fails.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Deploying a Contract
|
|
258
|
+
|
|
259
|
+
### Step 1: Upload Contract to TideCloak (Optional)
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
PUT /admin/realms/{realm}/tide-admin/forseti-contracts
|
|
263
|
+
Content-Type: application/json
|
|
264
|
+
Authorization: Bearer <admin-token>
|
|
265
|
+
|
|
266
|
+
{
|
|
267
|
+
"contractCode": "<C# source code as string>",
|
|
268
|
+
"name": "MyPolicy"
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The ORK compiles the contract in its sandbox and stores it. The contract ID is `SHA512(source code)`.
|
|
273
|
+
|
|
274
|
+
Note: This endpoint may not be available on all TideCloak images. If it returns 404/405, the contract is deployed through the policy signing flow (Step 3) instead — the contract source is included in the contract transport.
|
|
275
|
+
|
|
276
|
+
### Step 2: Create a Policy Using the Contract
|
|
277
|
+
|
|
278
|
+
On the JavaScript side, use `Models` from `@tidecloak/js` and `BasicCustomRequest` from `asgard-tide`:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { Models, Tools } from '@tidecloak/js'
|
|
282
|
+
const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models
|
|
283
|
+
const { TideMemory } = Tools
|
|
284
|
+
|
|
285
|
+
// Create the policy
|
|
286
|
+
const policy = new Policy({
|
|
287
|
+
contractId: contractHash, // SHA512 of the C# source
|
|
288
|
+
modelIds: ['MyModel:1'],
|
|
289
|
+
approvalType: ApprovalType.EXPLICIT, // or IMPLICIT
|
|
290
|
+
executionType: ExecutionType.PRIVATE, // or PUBLIC
|
|
291
|
+
params: [
|
|
292
|
+
['Role', 'data-access'],
|
|
293
|
+
['Resource', 'my-app'],
|
|
294
|
+
],
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Important:
|
|
299
|
+
- Import `Models` from `@tidecloak/js`, NOT from `@tidecloak/nextjs` (returns `undefined`)
|
|
300
|
+
- Import `BasicCustomRequest` from `asgard-tide` for signing requests
|
|
301
|
+
- Import `PolicySignRequest` from `heimdall-tide` for policy deployment (alternative flow)
|
|
302
|
+
- Policy params must be `[key, value]` pairs, not a plain object (AP-54)
|
|
303
|
+
|
|
304
|
+
### Step 3: Deploy the Policy to the ORK Network
|
|
305
|
+
|
|
306
|
+
Policy deployment requires the realm's **admin policy** to authorize the operation. The admin policy is pre-signed during realm setup (IGA bootstrap) and must be attached to every policy creation request. VERIFIED (LEARNINGS-ratidefy-batch-001 L-22).
|
|
307
|
+
|
|
308
|
+
**Fetch the admin policy** (server-side — endpoint does not support CORS):
|
|
309
|
+
```typescript
|
|
310
|
+
// Server-side: proxy this to the browser, or pre-download as static file
|
|
311
|
+
const adminPolicyUrl = `${authServerUrl}/realms/${realm}/tide-policy-resources/admin-policy`;
|
|
312
|
+
const adminPolicyB64 = await fetch(adminPolicyUrl).then(r => r.text());
|
|
313
|
+
const adminPolicyBytes = Uint8Array.from(atob(adminPolicyB64), c => c.charCodeAt(0));
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Build and deploy the policy** (browser-side — requires authenticated TideCloak session):
|
|
317
|
+
```typescript
|
|
318
|
+
const tc = (IAMService as any)._tc; // Internal TideCloak instance
|
|
319
|
+
|
|
320
|
+
// 1. Build the policy bytes and contract transport
|
|
321
|
+
const policyBytes = policy.toBytes();
|
|
322
|
+
const contractSource = `using Ork.Forseti.Sdk;\n\npublic class Contract : IAccessPolicy { ... }`;
|
|
323
|
+
const contractTransport = TideMemory.CreateFromArray([
|
|
324
|
+
new Uint8Array(0), // empty bytes
|
|
325
|
+
TideMemory.CreateFromArray([
|
|
326
|
+
new TextEncoder().encode(contractSource),
|
|
327
|
+
new TextEncoder().encode("Contract"), // entry type
|
|
328
|
+
]),
|
|
329
|
+
]);
|
|
330
|
+
const draft = TideMemory.CreateFromArray([policyBytes, contractTransport]);
|
|
331
|
+
|
|
332
|
+
// 2. Build the request with admin policy
|
|
333
|
+
const request = new BaseTideRequest("Policy", "1", "Policy:1", draft);
|
|
334
|
+
request.addAuthorizer(dokenBytes); // Admin's doken
|
|
335
|
+
request.policy = new TideMemory(adminPolicyBytes.length);
|
|
336
|
+
request.policy.set(adminPolicyBytes); // CRITICAL — without this, ORK rejects
|
|
337
|
+
|
|
338
|
+
// 3. Send to ORK network
|
|
339
|
+
const signRequest = await tc.createTideRequest(request.encode());
|
|
340
|
+
|
|
341
|
+
// 4. Operator approval via Tide enclave popup
|
|
342
|
+
const result = await tc.requestTideOperatorApproval([
|
|
343
|
+
{ id: "policy-deploy", request: signRequest }
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
// 5. Execute — ORK produces VVK signature
|
|
347
|
+
const sigs = await tc.executeSignRequest(result[0].request, true);
|
|
348
|
+
const vvkSignature = sigs[0]; // 64-byte Ed25519
|
|
349
|
+
|
|
350
|
+
// 6. Attach VVK signature to policy and store
|
|
351
|
+
policy.signature = new TideMemory(vvkSignature.length);
|
|
352
|
+
policy.signature.set(vvkSignature);
|
|
353
|
+
const signedPolicyBytes = policy.toBytes(); // Store THESE bytes
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Critical notes**:
|
|
357
|
+
- Use `BaseTideRequest` directly, NOT `PolicySignRequest` from `heimdall-tide` — the full flow requires direct ORK interaction. VERIFIED (LEARNINGS-ratidefy-batch-001 L-24).
|
|
358
|
+
- Use `tc.createTideRequest()` → `tc.requestTideOperatorApproval()` → `tc.executeSignRequest()` — the React context's `initializeTideRequest` does not expose the approve/execute steps. VERIFIED (LEARNINGS-ratidefy-batch-001 L-24).
|
|
359
|
+
- `initializeTideRequest` returns a **new object** — it does NOT mutate in place. If using it, capture the return value (AP-59).
|
|
360
|
+
- Store `policy.toBytes()` (with signature attached), NOT `request.encode()` (AP-57).
|
|
361
|
+
- Admin policy is fetched from `/realms/{realm}/tide-policy-resources/admin-policy`. This endpoint has no CORS — fetch server-side and proxy to browser (see RB-008). VERIFIED (LEARNINGS-ratidefy-batch-001 L-22).
|
|
362
|
+
|
|
363
|
+
### Step 4: Use the Signed Policy
|
|
364
|
+
|
|
365
|
+
For encryption:
|
|
366
|
+
```typescript
|
|
367
|
+
const ciphertext = await iam.doEncrypt(
|
|
368
|
+
[{ data: plaintext, tags: ['mytag'] }],
|
|
369
|
+
signedPolicyBytes
|
|
370
|
+
)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
For decryption:
|
|
374
|
+
```typescript
|
|
375
|
+
const plaintext = await iam.doDecrypt(ciphertext, signedPolicyBytes)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The signed policy bytes must be stored (e.g., in your database or fetched via admin API) and provided on every encrypt/decrypt call. The ORKs execute the contract and verify the caller's doken against the policy rules.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Example: Multi-Approver with Time Restriction
|
|
383
|
+
|
|
384
|
+
```csharp
|
|
385
|
+
using Ork.Forseti.Sdk;
|
|
386
|
+
|
|
387
|
+
public class Contract : IAccessPolicy
|
|
388
|
+
{
|
|
389
|
+
[PolicyParam(Required = true)]
|
|
390
|
+
public string ApproverRole { get; set; }
|
|
391
|
+
|
|
392
|
+
[PolicyParam(Required = true)]
|
|
393
|
+
public string ApproverResource { get; set; }
|
|
394
|
+
|
|
395
|
+
[PolicyParam(Default = 2, Min = 1)]
|
|
396
|
+
public int MinApprovers { get; set; }
|
|
397
|
+
|
|
398
|
+
[PolicyParam(Required = true)]
|
|
399
|
+
public string ExecutorRole { get; set; }
|
|
400
|
+
|
|
401
|
+
[PolicyParam(Required = true)]
|
|
402
|
+
public string ExecutorResource { get; set; }
|
|
403
|
+
|
|
404
|
+
public PolicyDecision ValidateData(DataContext ctx)
|
|
405
|
+
{
|
|
406
|
+
// Only allow operations during business hours
|
|
407
|
+
return Decision
|
|
408
|
+
.RequireWeekday()
|
|
409
|
+
.RequireHourBetween(9, 17);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public PolicyDecision ValidateApprovers(ApproversContext ctx)
|
|
413
|
+
{
|
|
414
|
+
var approvers = DokenDto.WrapAll(ctx.Dokens);
|
|
415
|
+
|
|
416
|
+
return Decision
|
|
417
|
+
.RequireMinWithRole(approvers, MinApprovers, ApproverResource, ApproverRole);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public PolicyDecision ValidateExecutor(ExecutorContext ctx)
|
|
421
|
+
{
|
|
422
|
+
var executor = new DokenDto(ctx.Doken);
|
|
423
|
+
|
|
424
|
+
return Decision
|
|
425
|
+
.RequireNotExpired(executor)
|
|
426
|
+
.RequireRole(executor, ExecutorResource, ExecutorRole);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Example: Direction-Aware Encryption Contract
|
|
434
|
+
|
|
435
|
+
```csharp
|
|
436
|
+
using Ork.Forseti.Sdk;
|
|
437
|
+
|
|
438
|
+
public class Contract : IAccessPolicy
|
|
439
|
+
{
|
|
440
|
+
[PolicyParam(Required = true)]
|
|
441
|
+
public string EncryptRole { get; set; }
|
|
442
|
+
|
|
443
|
+
[PolicyParam(Required = true)]
|
|
444
|
+
public string DecryptRole { get; set; }
|
|
445
|
+
|
|
446
|
+
[PolicyParam(Required = true)]
|
|
447
|
+
public string Resource { get; set; }
|
|
448
|
+
|
|
449
|
+
public PolicyDecision ValidateData(DataContext ctx)
|
|
450
|
+
{
|
|
451
|
+
return PolicyDecision.Allow();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public PolicyDecision ValidateApprovers(ApproversContext ctx)
|
|
455
|
+
{
|
|
456
|
+
return PolicyDecision.Allow();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public PolicyDecision ValidateExecutor(ExecutorContext ctx)
|
|
460
|
+
{
|
|
461
|
+
var executor = new DokenDto(ctx.Doken);
|
|
462
|
+
|
|
463
|
+
if (ctx.RequestId == "PolicyEnabledEncryption:1")
|
|
464
|
+
{
|
|
465
|
+
return Decision
|
|
466
|
+
.RequireNotExpired(executor)
|
|
467
|
+
.RequireRole(executor, Resource, EncryptRole);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (ctx.RequestId == "PolicyEnabledDecryption:1")
|
|
471
|
+
{
|
|
472
|
+
return Decision
|
|
473
|
+
.RequireNotExpired(executor)
|
|
474
|
+
.RequireRole(executor, Resource, DecryptRole);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return PolicyDecision.Deny("Unknown operation");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Sandbox Restrictions
|
|
485
|
+
|
|
486
|
+
Contracts run in a sandboxed VM. The following are blocked:
|
|
487
|
+
|
|
488
|
+
- File I/O (`System.IO`)
|
|
489
|
+
- Network (`System.Net`)
|
|
490
|
+
- Threads (`System.Threading`)
|
|
491
|
+
- Reflection (`System.Reflection`)
|
|
492
|
+
- Process/diagnostics (`System.Diagnostics`)
|
|
493
|
+
- Console (`System.Console`)
|
|
494
|
+
- Non-deterministic calls (`DateTime.Now`, `Guid.NewGuid`, `Random`)
|
|
495
|
+
|
|
496
|
+
A contract using a blocked namespace compiles but fails IL vetting with `BadPolicy.ForbiddenCall` at upload time.
|
|
497
|
+
|
|
498
|
+
Available: `System`, `System.Linq`, `System.Collections.Generic`, `System.Text`, plus SDK types (`Ork.Forseti.Sdk`, `Cryptide`, `Ork.Shared`).
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Anti-Patterns
|
|
503
|
+
|
|
504
|
+
- **Wrong namespace**: Use `using Ork.Forseti.Sdk;` not `using Tide.Ork.Classes.Forseti;` (AP-56)
|
|
505
|
+
- **Wrong class name**: Class must be named `Contract` and implement `IAccessPolicy`
|
|
506
|
+
- **Wrong context properties**: Use `ctx.Dokens` (not `ctx.Approvers`) in `ValidateApprovers`, `ctx.Doken` (not `ctx.Executor`) in `ValidateExecutor`. Wrap with `DokenDto.WrapAll(ctx.Dokens)` and `new DokenDto(ctx.Doken)`. VERIFIED (LEARNINGS-ratidefy-batch-001 L-23).
|
|
507
|
+
- **Params as plain object**: Use `[['Role', 'admin']]` not `{ Role: 'admin' }` (AP-54)
|
|
508
|
+
- **Store request.encode() as policy**: Use `policy.toBytes()` not `request.encode()` (AP-57). Related: AP-55 (don't store raw VVK sig either)
|
|
509
|
+
- **Missing admin policy**: Policy deployment requires the realm's admin policy attached to the request. Fetch from `/realms/{realm}/tide-policy-resources/admin-policy` (AP, LEARNINGS-ratidefy-batch-001 L-22)
|
|
510
|
+
- **JSON to createTideRequest**: Pass `Uint8Array` from `signRequest.encode()` (AP-53)
|
|
511
|
+
- **Import BasicCustomRequest from wrong package**: Use `asgard-tide`, not `@tideorg/js` or `@tidecloak/js` (LEARNINGS-ratidefy-batch-001 L-11)
|
|
512
|
+
- **Import from @tidecloak/nextjs**: Use `@tidecloak/js` for Models, `heimdall-tide` for PolicySignRequest, `asgard-tide` for BasicCustomRequest
|
|
513
|
+
- **Call methods on IAMService**: Use `(IAMService as any)._tc` for `createTideRequest`, `executeSignRequest`
|
|
514
|
+
- **Use static IAMService.secureFetch/getToken in React apps**: Use `useTideCloak()` hook instead (AP-58)
|
|
515
|
+
- **Assume initializeTideRequest mutates in place**: It returns a new object — capture the return value (AP-59)
|
|
516
|
+
- **Check `_tide_*` roles in contract**: Those are voucher gates, not access control. Use regular roles (AP-25)
|
|
517
|
+
- **Client-side policy logic**: Forseti runs on ORKs, not in the browser (AP-11)
|
|
518
|
+
- **`PolicyDecision.Approve()`**: Does not exist. Use `PolicyDecision.Allow()`
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Error Messages
|
|
523
|
+
|
|
524
|
+
Contract errors propagate to the client as strings:
|
|
525
|
+
|
|
526
|
+
| Error | Meaning |
|
|
527
|
+
|-------|---------|
|
|
528
|
+
| `PolicyDecision.Deny("message")` | Contract explicitly denied |
|
|
529
|
+
| `BadPolicy.ForbiddenCall:{target}` | Contract used a blocked namespace |
|
|
530
|
+
| `BadPolicy.EntryTypeNotFound` | Class `Contract` not found or doesn't implement `IAccessPolicy` |
|
|
531
|
+
| `BadPolicy.BudgetExceeded` | Gas limit exceeded |
|
|
532
|
+
| `VmHost.Timeout` | Contract took too long |
|
|
533
|
+
| `OutOfGasException` | Gas exhausted mid-execution |
|