@feltdb/core 0.5.6 → 0.6.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/dist/cli/commands.js +68 -1
- package/dist/cli/workspace-integration.js +96 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +21 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +10 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
- package/dist/create/template/dot-feltdb-README.md +58 -0
- package/dist/create/workspace-initialization.js +77 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
- package/dist/studio-app/index.html +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/development-node.d.ts +42 -0
- package/dist/workspace/development-node.d.ts.map +1 -0
- package/dist/workspace/development-node.js +208 -0
- package/dist/workspace/index.d.ts +20 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +16 -0
- package/dist/workspace/workspace-connection.d.ts +174 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -0
- package/dist/workspace/workspace-connection.js +290 -0
- package/dist/workspace/workspace-identity.d.ts +13 -0
- package/dist/workspace/workspace-identity.d.ts.map +1 -0
- package/dist/workspace/workspace-identity.js +70 -0
- package/dist/workspace/workspace-types.d.ts +82 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -0
- package/dist/workspace/workspace-types.js +7 -0
- package/package.json +5 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use sha2::{Digest, Sha256};
|
|
3
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
4
|
+
|
|
5
|
+
/// Signed delegation token for federated identity.
|
|
6
|
+
/// Enables Sherpa to delegate operations to FeltDB without direct credentials.
|
|
7
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
8
|
+
pub struct DelegationToken {
|
|
9
|
+
/// Unique token ID for idempotency and revocation tracking.
|
|
10
|
+
pub token_id: String,
|
|
11
|
+
|
|
12
|
+
/// Service that issued this token (e.g., "sherpa", "apple", "stripe").
|
|
13
|
+
pub issuer: String,
|
|
14
|
+
|
|
15
|
+
/// Service this token is intended for (e.g., "feltdb").
|
|
16
|
+
pub audience: String,
|
|
17
|
+
|
|
18
|
+
/// ID of the service key used to sign this token.
|
|
19
|
+
pub service_key_id: String,
|
|
20
|
+
|
|
21
|
+
/// Actor ID this token delegates to.
|
|
22
|
+
pub actor_id: String,
|
|
23
|
+
|
|
24
|
+
/// Tenant ID this token is scoped to.
|
|
25
|
+
pub tenant_id: String,
|
|
26
|
+
|
|
27
|
+
/// Roles granted by this delegation.
|
|
28
|
+
pub roles: Vec<String>,
|
|
29
|
+
|
|
30
|
+
/// Scope constraint for this delegation (e.g., "read_only", "write_release_jobs").
|
|
31
|
+
pub scope: DelegationScope,
|
|
32
|
+
|
|
33
|
+
/// When this token was issued (Unix seconds).
|
|
34
|
+
pub issued_at: u64,
|
|
35
|
+
|
|
36
|
+
/// When this token expires (Unix seconds).
|
|
37
|
+
pub expires_at: u64,
|
|
38
|
+
|
|
39
|
+
/// Signature of the token (issuer must verify).
|
|
40
|
+
pub signature: String,
|
|
41
|
+
|
|
42
|
+
/// Hash algorithm used for signature (e.g., "sha256").
|
|
43
|
+
pub signature_algorithm: String,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
47
|
+
#[serde(rename_all = "snake_case")]
|
|
48
|
+
pub enum DelegationScope {
|
|
49
|
+
Unrestricted,
|
|
50
|
+
ReadOnly,
|
|
51
|
+
WriteReleaseJobs,
|
|
52
|
+
ExecuteWorkflows,
|
|
53
|
+
ManagePrincipals,
|
|
54
|
+
Custom(String),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl DelegationScope {
|
|
58
|
+
pub fn allows_operation(&self, operation: &str) -> bool {
|
|
59
|
+
match self {
|
|
60
|
+
DelegationScope::Unrestricted => true,
|
|
61
|
+
DelegationScope::ReadOnly => operation == "read",
|
|
62
|
+
DelegationScope::WriteReleaseJobs => {
|
|
63
|
+
operation == "read" || operation == "write:release_jobs"
|
|
64
|
+
}
|
|
65
|
+
DelegationScope::ExecuteWorkflows => {
|
|
66
|
+
operation == "read" || operation == "execute:workflows"
|
|
67
|
+
}
|
|
68
|
+
DelegationScope::ManagePrincipals => {
|
|
69
|
+
operation == "read" || operation == "write:principals"
|
|
70
|
+
}
|
|
71
|
+
DelegationScope::Custom(constraint) => operation.starts_with(constraint),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[derive(Debug, Clone)]
|
|
77
|
+
pub struct DelegationTokenIssuer {
|
|
78
|
+
/// Shared secret for signing (in production, loaded from HSM).
|
|
79
|
+
/// Both issuer and validator must have the same secret.
|
|
80
|
+
shared_secret: Vec<u8>,
|
|
81
|
+
/// Service name (e.g., "sherpa").
|
|
82
|
+
service_name: String,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[derive(Debug, Clone)]
|
|
86
|
+
pub struct DelegationTokenValidator {
|
|
87
|
+
/// Shared secret for verification (same as issuer's).
|
|
88
|
+
shared_secret: Vec<u8>,
|
|
89
|
+
/// Expected issuer service name.
|
|
90
|
+
expected_issuer: String,
|
|
91
|
+
/// Audience we expect (e.g., "feltdb").
|
|
92
|
+
expected_audience: String,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl DelegationTokenIssuer {
|
|
96
|
+
/// Creates a new token issuer (for testing; production uses HSM).
|
|
97
|
+
pub fn new(service_name: String, shared_secret: Vec<u8>) -> Self {
|
|
98
|
+
Self {
|
|
99
|
+
shared_secret,
|
|
100
|
+
service_name,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Issues a new delegation token.
|
|
105
|
+
pub fn issue(
|
|
106
|
+
&self,
|
|
107
|
+
service_key_id: String,
|
|
108
|
+
actor_id: String,
|
|
109
|
+
tenant_id: String,
|
|
110
|
+
roles: Vec<String>,
|
|
111
|
+
scope: DelegationScope,
|
|
112
|
+
ttl_seconds: u64,
|
|
113
|
+
) -> Result<DelegationToken, String> {
|
|
114
|
+
let now = now();
|
|
115
|
+
let token_id = generate_token_id();
|
|
116
|
+
|
|
117
|
+
let token = DelegationToken {
|
|
118
|
+
token_id: token_id.clone(),
|
|
119
|
+
issuer: self.service_name.clone(),
|
|
120
|
+
audience: "feltdb".into(),
|
|
121
|
+
service_key_id,
|
|
122
|
+
actor_id,
|
|
123
|
+
tenant_id,
|
|
124
|
+
roles,
|
|
125
|
+
scope,
|
|
126
|
+
issued_at: now,
|
|
127
|
+
expires_at: now + ttl_seconds,
|
|
128
|
+
signature: String::new(),
|
|
129
|
+
signature_algorithm: "sha256-hmac".into(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let payload = self.canonicalize_for_signing(&token)?;
|
|
133
|
+
let signature = self.sign(&payload)?;
|
|
134
|
+
|
|
135
|
+
Ok(DelegationToken {
|
|
136
|
+
signature,
|
|
137
|
+
..token
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Canonicalizes token for consistent signing/verification.
|
|
142
|
+
fn canonicalize_for_signing(&self, token: &DelegationToken) -> Result<String, String> {
|
|
143
|
+
let mut hasher = Sha256::new();
|
|
144
|
+
|
|
145
|
+
hasher.update(token.token_id.as_bytes());
|
|
146
|
+
hasher.update(token.issuer.as_bytes());
|
|
147
|
+
hasher.update(token.audience.as_bytes());
|
|
148
|
+
hasher.update(token.service_key_id.as_bytes());
|
|
149
|
+
hasher.update(token.actor_id.as_bytes());
|
|
150
|
+
hasher.update(token.tenant_id.as_bytes());
|
|
151
|
+
hasher.update(token.issued_at.to_le_bytes());
|
|
152
|
+
hasher.update(token.expires_at.to_le_bytes());
|
|
153
|
+
|
|
154
|
+
for role in &token.roles {
|
|
155
|
+
hasher.update(role.as_bytes());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let scope_str = serde_json::to_string(&token.scope)
|
|
159
|
+
.map_err(|e| format!("failed to serialize scope: {e}"))?;
|
|
160
|
+
hasher.update(scope_str.as_bytes());
|
|
161
|
+
|
|
162
|
+
Ok(format!("{:x}", hasher.finalize()))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Signs the canonicalized payload.
|
|
166
|
+
/// In production, this would use HSM/KMS.
|
|
167
|
+
fn sign(&self, payload: &str) -> Result<String, String> {
|
|
168
|
+
use hmac::{Hmac, Mac};
|
|
169
|
+
use sha2::Sha256;
|
|
170
|
+
|
|
171
|
+
type HmacSha256 = Hmac<Sha256>;
|
|
172
|
+
|
|
173
|
+
let mut mac = HmacSha256::new_from_slice(&self.shared_secret)
|
|
174
|
+
.map_err(|e| format!("invalid shared secret: {e}"))?;
|
|
175
|
+
mac.update(payload.as_bytes());
|
|
176
|
+
|
|
177
|
+
Ok(format!("{:x}", mac.finalize().into_bytes()))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
impl DelegationTokenValidator {
|
|
182
|
+
/// Creates a new token validator.
|
|
183
|
+
pub fn new(
|
|
184
|
+
expected_issuer: String,
|
|
185
|
+
expected_audience: String,
|
|
186
|
+
shared_secret: Vec<u8>,
|
|
187
|
+
) -> Self {
|
|
188
|
+
Self {
|
|
189
|
+
expected_issuer,
|
|
190
|
+
expected_audience,
|
|
191
|
+
shared_secret,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Validates a delegation token.
|
|
196
|
+
pub fn validate(&self, token: &DelegationToken) -> Result<(), String> {
|
|
197
|
+
self.check_not_expired(token)?;
|
|
198
|
+
self.check_issuer(token)?;
|
|
199
|
+
self.check_audience(token)?;
|
|
200
|
+
self.check_signature(token)?;
|
|
201
|
+
|
|
202
|
+
Ok(())
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn check_not_expired(&self, token: &DelegationToken) -> Result<(), String> {
|
|
206
|
+
let now = now();
|
|
207
|
+
if now > token.expires_at {
|
|
208
|
+
return Err(format!(
|
|
209
|
+
"token expired at {} (current time: {})",
|
|
210
|
+
token.expires_at, now
|
|
211
|
+
));
|
|
212
|
+
}
|
|
213
|
+
Ok(())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn check_issuer(&self, token: &DelegationToken) -> Result<(), String> {
|
|
217
|
+
if token.issuer != self.expected_issuer {
|
|
218
|
+
return Err(format!(
|
|
219
|
+
"issuer mismatch: expected {} but got {}",
|
|
220
|
+
self.expected_issuer, token.issuer
|
|
221
|
+
));
|
|
222
|
+
}
|
|
223
|
+
Ok(())
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn check_audience(&self, token: &DelegationToken) -> Result<(), String> {
|
|
227
|
+
if token.audience != self.expected_audience {
|
|
228
|
+
return Err(format!(
|
|
229
|
+
"audience mismatch: expected {} but got {}",
|
|
230
|
+
self.expected_audience, token.audience
|
|
231
|
+
));
|
|
232
|
+
}
|
|
233
|
+
Ok(())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn check_signature(&self, token: &DelegationToken) -> Result<(), String> {
|
|
237
|
+
let mut token_copy = token.clone();
|
|
238
|
+
let original_sig = token_copy.signature.clone();
|
|
239
|
+
token_copy.signature = String::new();
|
|
240
|
+
|
|
241
|
+
let payload = self.canonicalize_for_signing(&token_copy)?;
|
|
242
|
+
self.verify_signature(&payload, &original_sig)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// Canonicalizes token for verification (mirrors issuer logic).
|
|
246
|
+
fn canonicalize_for_signing(&self, token: &DelegationToken) -> Result<String, String> {
|
|
247
|
+
let mut hasher = Sha256::new();
|
|
248
|
+
|
|
249
|
+
hasher.update(token.token_id.as_bytes());
|
|
250
|
+
hasher.update(token.issuer.as_bytes());
|
|
251
|
+
hasher.update(token.audience.as_bytes());
|
|
252
|
+
hasher.update(token.service_key_id.as_bytes());
|
|
253
|
+
hasher.update(token.actor_id.as_bytes());
|
|
254
|
+
hasher.update(token.tenant_id.as_bytes());
|
|
255
|
+
hasher.update(token.issued_at.to_le_bytes());
|
|
256
|
+
hasher.update(token.expires_at.to_le_bytes());
|
|
257
|
+
|
|
258
|
+
for role in &token.roles {
|
|
259
|
+
hasher.update(role.as_bytes());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let scope_str = serde_json::to_string(&token.scope)
|
|
263
|
+
.map_err(|e| format!("failed to serialize scope: {e}"))?;
|
|
264
|
+
hasher.update(scope_str.as_bytes());
|
|
265
|
+
|
|
266
|
+
Ok(format!("{:x}", hasher.finalize()))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Verifies signature using the shared secret.
|
|
270
|
+
fn verify_signature(&self, payload: &str, signature: &str) -> Result<(), String> {
|
|
271
|
+
use hmac::{Hmac, Mac};
|
|
272
|
+
use sha2::Sha256;
|
|
273
|
+
|
|
274
|
+
type HmacSha256 = Hmac<Sha256>;
|
|
275
|
+
|
|
276
|
+
let mut mac = HmacSha256::new_from_slice(&self.shared_secret)
|
|
277
|
+
.map_err(|e| format!("invalid shared secret: {e}"))?;
|
|
278
|
+
mac.update(payload.as_bytes());
|
|
279
|
+
|
|
280
|
+
let expected_sig = format!("{:x}", mac.finalize().into_bytes());
|
|
281
|
+
if expected_sig == signature {
|
|
282
|
+
Ok(())
|
|
283
|
+
} else {
|
|
284
|
+
Err("signature verification failed".into())
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fn now() -> u64 {
|
|
290
|
+
SystemTime::now()
|
|
291
|
+
.duration_since(UNIX_EPOCH)
|
|
292
|
+
.unwrap_or_default()
|
|
293
|
+
.as_secs()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn generate_token_id() -> String {
|
|
297
|
+
use rand::{distributions::Alphanumeric, Rng};
|
|
298
|
+
|
|
299
|
+
let value: String = rand::thread_rng()
|
|
300
|
+
.sample_iter(&Alphanumeric)
|
|
301
|
+
.take(32)
|
|
302
|
+
.map(char::from)
|
|
303
|
+
.collect();
|
|
304
|
+
format!("tok_{}", value)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[cfg(test)]
|
|
308
|
+
mod tests {
|
|
309
|
+
use super::*;
|
|
310
|
+
|
|
311
|
+
fn test_shared_secret() -> Vec<u8> {
|
|
312
|
+
b"test_shared_secret_32_bytes______".to_vec()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[test]
|
|
316
|
+
fn issue_and_validate_delegation_token() {
|
|
317
|
+
let secret = test_shared_secret();
|
|
318
|
+
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
319
|
+
let validator =
|
|
320
|
+
DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
321
|
+
|
|
322
|
+
let token = issuer
|
|
323
|
+
.issue(
|
|
324
|
+
"key_sherpa".into(),
|
|
325
|
+
"app_acme".into(),
|
|
326
|
+
"tenant_prod".into(),
|
|
327
|
+
vec!["execute".into()],
|
|
328
|
+
DelegationScope::ExecuteWorkflows,
|
|
329
|
+
3600,
|
|
330
|
+
)
|
|
331
|
+
.expect("failed to issue token");
|
|
332
|
+
|
|
333
|
+
assert!(validator.validate(&token).is_ok());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[test]
|
|
337
|
+
fn reject_expired_token() {
|
|
338
|
+
let secret = test_shared_secret();
|
|
339
|
+
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
340
|
+
let validator =
|
|
341
|
+
DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
342
|
+
|
|
343
|
+
let mut token = issuer
|
|
344
|
+
.issue(
|
|
345
|
+
"key_sherpa".into(),
|
|
346
|
+
"app_acme".into(),
|
|
347
|
+
"tenant_prod".into(),
|
|
348
|
+
vec!["execute".into()],
|
|
349
|
+
DelegationScope::ExecuteWorkflows,
|
|
350
|
+
1, // 1 second TTL
|
|
351
|
+
)
|
|
352
|
+
.expect("failed to issue token");
|
|
353
|
+
|
|
354
|
+
// Simulate expiration
|
|
355
|
+
token.expires_at = now() - 1;
|
|
356
|
+
|
|
357
|
+
assert!(validator.validate(&token).is_err());
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[test]
|
|
361
|
+
fn reject_wrong_issuer() {
|
|
362
|
+
let secret = test_shared_secret();
|
|
363
|
+
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
364
|
+
let validator =
|
|
365
|
+
DelegationTokenValidator::new("other_service".into(), "feltdb".into(), secret);
|
|
366
|
+
|
|
367
|
+
let token = issuer
|
|
368
|
+
.issue(
|
|
369
|
+
"key_sherpa".into(),
|
|
370
|
+
"app_acme".into(),
|
|
371
|
+
"tenant_prod".into(),
|
|
372
|
+
vec!["execute".into()],
|
|
373
|
+
DelegationScope::ExecuteWorkflows,
|
|
374
|
+
3600,
|
|
375
|
+
)
|
|
376
|
+
.expect("failed to issue token");
|
|
377
|
+
|
|
378
|
+
assert!(validator.validate(&token).is_err());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#[test]
|
|
382
|
+
fn reject_wrong_audience() {
|
|
383
|
+
let secret = test_shared_secret();
|
|
384
|
+
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
385
|
+
let validator =
|
|
386
|
+
DelegationTokenValidator::new("sherpa".into(), "other_service".into(), secret);
|
|
387
|
+
|
|
388
|
+
let token = issuer
|
|
389
|
+
.issue(
|
|
390
|
+
"key_sherpa".into(),
|
|
391
|
+
"app_acme".into(),
|
|
392
|
+
"tenant_prod".into(),
|
|
393
|
+
vec!["execute".into()],
|
|
394
|
+
DelegationScope::ExecuteWorkflows,
|
|
395
|
+
3600,
|
|
396
|
+
)
|
|
397
|
+
.expect("failed to issue token");
|
|
398
|
+
|
|
399
|
+
assert!(validator.validate(&token).is_err());
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#[test]
|
|
403
|
+
fn scope_enforces_constraints() {
|
|
404
|
+
let read_only = DelegationScope::ReadOnly;
|
|
405
|
+
assert!(read_only.allows_operation("read"));
|
|
406
|
+
assert!(!read_only.allows_operation("write"));
|
|
407
|
+
|
|
408
|
+
let write_jobs = DelegationScope::WriteReleaseJobs;
|
|
409
|
+
assert!(write_jobs.allows_operation("read"));
|
|
410
|
+
assert!(write_jobs.allows_operation("write:release_jobs"));
|
|
411
|
+
assert!(!write_jobs.allows_operation("write:principals"));
|
|
412
|
+
|
|
413
|
+
let unrestricted = DelegationScope::Unrestricted;
|
|
414
|
+
assert!(unrestricted.allows_operation("read"));
|
|
415
|
+
assert!(unrestricted.allows_operation("write"));
|
|
416
|
+
assert!(unrestricted.allows_operation("delete"));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn token_id_is_unique() {
|
|
421
|
+
let secret = test_shared_secret();
|
|
422
|
+
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret);
|
|
423
|
+
|
|
424
|
+
let token1 = issuer
|
|
425
|
+
.issue(
|
|
426
|
+
"key_sherpa".into(),
|
|
427
|
+
"app_acme".into(),
|
|
428
|
+
"tenant_prod".into(),
|
|
429
|
+
vec![],
|
|
430
|
+
DelegationScope::ReadOnly,
|
|
431
|
+
3600,
|
|
432
|
+
)
|
|
433
|
+
.expect("failed to issue token 1");
|
|
434
|
+
|
|
435
|
+
let token2 = issuer
|
|
436
|
+
.issue(
|
|
437
|
+
"key_sherpa".into(),
|
|
438
|
+
"app_acme".into(),
|
|
439
|
+
"tenant_prod".into(),
|
|
440
|
+
vec![],
|
|
441
|
+
DelegationScope::ReadOnly,
|
|
442
|
+
3600,
|
|
443
|
+
)
|
|
444
|
+
.expect("failed to issue token 2");
|
|
445
|
+
|
|
446
|
+
assert_ne!(token1.token_id, token2.token_id);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn tampering_invalidates_signature() {
|
|
451
|
+
let secret = test_shared_secret();
|
|
452
|
+
let issuer = DelegationTokenIssuer::new("sherpa".into(), secret.clone());
|
|
453
|
+
let validator =
|
|
454
|
+
DelegationTokenValidator::new("sherpa".into(), "feltdb".into(), secret);
|
|
455
|
+
|
|
456
|
+
let mut token = issuer
|
|
457
|
+
.issue(
|
|
458
|
+
"key_sherpa".into(),
|
|
459
|
+
"app_acme".into(),
|
|
460
|
+
"tenant_prod".into(),
|
|
461
|
+
vec!["execute".into()],
|
|
462
|
+
DelegationScope::ExecuteWorkflows,
|
|
463
|
+
3600,
|
|
464
|
+
)
|
|
465
|
+
.expect("failed to issue token");
|
|
466
|
+
|
|
467
|
+
// Tamper with the token
|
|
468
|
+
token.actor_id = "app_other".into();
|
|
469
|
+
|
|
470
|
+
assert!(validator.validate(&token).is_err());
|
|
471
|
+
}
|
|
472
|
+
}
|