1extern crate biscuit_auth as biscuit;
2
3use biscuit::macros::{biscuit, check};
4use chrono::Utc;
5use hessra_token_core::{KeyPair, TokenTimeConfig};
6use std::error::Error;
7use tracing::info;
8
9pub struct HessraCapability {
33 subject: Option<String>,
34 resource: Option<String>,
35 operation: Option<String>,
36 time_config: TokenTimeConfig,
37 namespace: Option<String>,
38}
39
40impl HessraCapability {
41 pub fn new(
49 subject: String,
50 resource: String,
51 operation: String,
52 time_config: TokenTimeConfig,
53 ) -> Self {
54 Self {
55 subject: Some(subject),
56 resource: Some(resource),
57 operation: Some(operation),
58 time_config,
59 namespace: None,
60 }
61 }
62
63 pub fn namespace_restricted(mut self, namespace: String) -> Self {
71 self.namespace = Some(namespace);
72 self
73 }
74
75 pub fn issue(self, keypair: &KeyPair) -> Result<String, Box<dyn Error>> {
83 let start_time = self
84 .time_config
85 .start_time
86 .unwrap_or_else(|| Utc::now().timestamp());
87 let expiration = start_time + self.time_config.duration;
88
89 let namespace = self.namespace;
90
91 let subject = self.subject.ok_or("Token requires subject")?;
92 let resource = self.resource.ok_or("Token requires resource")?;
93 let operation = self.operation.ok_or("Token requires operation")?;
94
95 let mut biscuit_builder = biscuit!(
100 r#"
101 right({subject}, {resource}, {operation});
102 check if resource($res), operation($op), right($sub, $res, $op);
103 check if time($time), $time < {expiration};
104 "#
105 );
106
107 if let Some(namespace) = namespace {
109 biscuit_builder = biscuit_builder.check(check!(
110 r#"
111 check if namespace({namespace});
112 "#
113 ))?;
114 }
115
116 let biscuit = biscuit_builder.build(keypair)?;
118 info!("biscuit (authority): {}", biscuit);
119 let token = biscuit.to_base64()?;
120 Ok(token)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::verify::CapabilityVerifier;
128 use chrono::Utc;
129
130 #[test]
131 fn test_create_and_verify_capability() {
132 let subject = "test@test.com".to_owned();
133 let resource = "res1".to_string();
134 let operation = "read".to_string();
135 let root = KeyPair::new();
136 let public_key = root.public();
137
138 let token = HessraCapability::new(
139 subject.clone(),
140 resource.clone(),
141 operation.clone(),
142 TokenTimeConfig::default(),
143 )
144 .issue(&root)
145 .expect("Failed to create token");
146
147 let res = CapabilityVerifier::new(token, public_key, resource, operation).verify();
148 assert!(res.is_ok());
149 }
150
151 #[test]
152 fn test_capability_without_subject() {
153 let subject = "alice".to_owned();
154 let resource = "res1".to_string();
155 let operation = "read".to_string();
156 let root = KeyPair::new();
157 let public_key = root.public();
158
159 let token = HessraCapability::new(
160 subject.clone(),
161 resource.clone(),
162 operation.clone(),
163 TokenTimeConfig::default(),
164 )
165 .issue(&root)
166 .expect("Failed to create token");
167
168 let res = CapabilityVerifier::new(token, public_key, resource, operation).verify();
170 assert!(
171 res.is_ok(),
172 "Capability verification without subject should succeed"
173 );
174 }
175
176 #[test]
177 fn test_capability_with_optional_subject() {
178 let subject = "alice".to_owned();
179 let resource = "res1".to_string();
180 let operation = "read".to_string();
181 let root = KeyPair::new();
182 let public_key = root.public();
183
184 let token = HessraCapability::new(
185 subject.clone(),
186 resource.clone(),
187 operation.clone(),
188 TokenTimeConfig::default(),
189 )
190 .issue(&root)
191 .expect("Failed to create token");
192
193 let res = CapabilityVerifier::new(
195 token.clone(),
196 public_key,
197 resource.clone(),
198 operation.clone(),
199 )
200 .with_subject(subject.clone())
201 .verify();
202 assert!(
203 res.is_ok(),
204 "Verification with correct subject should succeed"
205 );
206
207 let res = CapabilityVerifier::new(
209 token.clone(),
210 public_key,
211 resource.clone(),
212 operation.clone(),
213 )
214 .with_subject("bob".to_string())
215 .verify();
216 assert!(res.is_err(), "Verification with wrong subject should fail");
217 }
218
219 #[test]
220 fn test_wrong_resource_rejected() {
221 let root = KeyPair::new();
222 let public_key = root.public();
223
224 let token = HessraCapability::new(
225 "alice".to_string(),
226 "res1".to_string(),
227 "read".to_string(),
228 TokenTimeConfig::default(),
229 )
230 .issue(&root)
231 .expect("Failed to create token");
232
233 let res =
234 CapabilityVerifier::new(token, public_key, "res2".to_string(), "read".to_string())
235 .verify();
236 assert!(res.is_err(), "Wrong resource should be rejected");
237 }
238
239 #[test]
240 fn test_wrong_operation_rejected() {
241 let root = KeyPair::new();
242 let public_key = root.public();
243
244 let token = HessraCapability::new(
245 "alice".to_string(),
246 "res1".to_string(),
247 "read".to_string(),
248 TokenTimeConfig::default(),
249 )
250 .issue(&root)
251 .expect("Failed to create token");
252
253 let res =
254 CapabilityVerifier::new(token, public_key, "res1".to_string(), "write".to_string())
255 .verify();
256 assert!(res.is_err(), "Wrong operation should be rejected");
257 }
258
259 #[test]
260 fn test_biscuit_expiration() {
261 let subject = "test@test.com".to_owned();
262 let resource = "res1".to_string();
263 let operation = "read".to_string();
264 let root = KeyPair::new();
265 let public_key = root.public();
266
267 let token = HessraCapability::new(
269 subject.clone(),
270 resource.clone(),
271 operation.clone(),
272 TokenTimeConfig::default(),
273 )
274 .issue(&root)
275 .expect("Failed to create token");
276
277 let res = CapabilityVerifier::new(token, public_key, resource.clone(), operation.clone())
278 .verify();
279 assert!(res.is_ok());
280
281 let root = KeyPair::new();
283 let public_key = root.public();
284 let token = HessraCapability::new(
285 subject.clone(),
286 resource.clone(),
287 operation.clone(),
288 TokenTimeConfig {
289 start_time: Some(Utc::now().timestamp() - 301),
290 duration: 300,
291 },
292 )
293 .issue(&root)
294 .expect("Failed to create expired token");
295
296 let res = CapabilityVerifier::new(token, public_key, resource, operation).verify();
297 assert!(res.is_err(), "Expired token should be rejected");
298 }
299
300 #[test]
301 fn test_namespace_restricted_capability() {
302 let keypair = KeyPair::new();
303 let public_key = keypair.public();
304
305 let token = HessraCapability::new(
306 "alice".to_string(),
307 "resource1".to_string(),
308 "read".to_string(),
309 TokenTimeConfig::default(),
310 )
311 .namespace_restricted("myapp.hessra.dev".to_string())
312 .issue(&keypair)
313 .expect("Failed to create namespace-restricted token");
314
315 let res = CapabilityVerifier::new(
317 token.clone(),
318 public_key,
319 "resource1".to_string(),
320 "read".to_string(),
321 )
322 .with_namespace("myapp.hessra.dev".to_string())
323 .verify();
324 assert!(res.is_ok(), "Should pass with matching namespace");
325
326 let res = CapabilityVerifier::new(
328 token.clone(),
329 public_key,
330 "resource1".to_string(),
331 "read".to_string(),
332 )
333 .verify();
334 assert!(res.is_err(), "Should fail without namespace");
335
336 let res = CapabilityVerifier::new(
338 token.clone(),
339 public_key,
340 "resource1".to_string(),
341 "read".to_string(),
342 )
343 .with_namespace("wrong.com".to_string())
344 .verify();
345 assert!(res.is_err(), "Should fail with wrong namespace");
346 }
347
348 #[test]
349 fn test_designation_attenuation() {
350 let keypair = KeyPair::new();
351 let public_key = keypair.public();
352
353 let token = HessraCapability::new(
355 "alice".to_string(),
356 "resource1".to_string(),
357 "read".to_string(),
358 TokenTimeConfig::default(),
359 )
360 .issue(&keypair)
361 .expect("Failed to create token");
362
363 let attenuated = crate::attenuate::DesignationBuilder::from_base64(token, public_key)
365 .expect("Failed to create designation builder")
366 .designate("tenant_id".to_string(), "t-123".to_string())
367 .attenuate_base64()
368 .expect("Failed to attenuate");
369
370 let res = CapabilityVerifier::new(
372 attenuated.clone(),
373 public_key,
374 "resource1".to_string(),
375 "read".to_string(),
376 )
377 .with_designation("tenant_id".to_string(), "t-123".to_string())
378 .verify();
379 assert!(res.is_ok(), "Should pass with matching designation");
380
381 let res = CapabilityVerifier::new(
383 attenuated.clone(),
384 public_key,
385 "resource1".to_string(),
386 "read".to_string(),
387 )
388 .with_designation("tenant_id".to_string(), "t-999".to_string())
389 .verify();
390 assert!(res.is_err(), "Should fail with wrong designation value");
391
392 let res = CapabilityVerifier::new(
394 attenuated.clone(),
395 public_key,
396 "resource1".to_string(),
397 "read".to_string(),
398 )
399 .verify();
400 assert!(res.is_err(), "Should fail without designation");
401 }
402
403 #[test]
404 fn test_multi_designation() {
405 let keypair = KeyPair::new();
406 let public_key = keypair.public();
407
408 let token = HessraCapability::new(
409 "alice".to_string(),
410 "resource1".to_string(),
411 "read".to_string(),
412 TokenTimeConfig::default(),
413 )
414 .issue(&keypair)
415 .expect("Failed to create token");
416
417 let attenuated = crate::attenuate::DesignationBuilder::from_base64(token, public_key)
419 .expect("Failed to create designation builder")
420 .designate("tenant_id".to_string(), "t-123".to_string())
421 .designate("user_id".to_string(), "u-456".to_string())
422 .attenuate_base64()
423 .expect("Failed to attenuate");
424
425 let res = CapabilityVerifier::new(
427 attenuated.clone(),
428 public_key,
429 "resource1".to_string(),
430 "read".to_string(),
431 )
432 .with_designation("tenant_id".to_string(), "t-123".to_string())
433 .with_designation("user_id".to_string(), "u-456".to_string())
434 .verify();
435 assert!(res.is_ok(), "Should pass with both designations");
436
437 let res = CapabilityVerifier::new(
439 attenuated.clone(),
440 public_key,
441 "resource1".to_string(),
442 "read".to_string(),
443 )
444 .with_designation("tenant_id".to_string(), "t-123".to_string())
445 .verify();
446 assert!(res.is_err(), "Should fail with missing designation");
447 }
448
449 #[test]
450 fn test_namespace_plus_designation() {
451 let keypair = KeyPair::new();
452 let public_key = keypair.public();
453
454 let token = HessraCapability::new(
455 "alice".to_string(),
456 "resource1".to_string(),
457 "read".to_string(),
458 TokenTimeConfig::default(),
459 )
460 .namespace_restricted("myapp.hessra.dev".to_string())
461 .issue(&keypair)
462 .expect("Failed to create token");
463
464 let attenuated = crate::attenuate::DesignationBuilder::from_base64(token, public_key)
466 .expect("Failed to create designation builder")
467 .designate("tenant_id".to_string(), "t-123".to_string())
468 .attenuate_base64()
469 .expect("Failed to attenuate");
470
471 let res = CapabilityVerifier::new(
473 attenuated.clone(),
474 public_key,
475 "resource1".to_string(),
476 "read".to_string(),
477 )
478 .with_namespace("myapp.hessra.dev".to_string())
479 .with_designation("tenant_id".to_string(), "t-123".to_string())
480 .verify();
481 assert!(
482 res.is_ok(),
483 "Should pass with both namespace and designation"
484 );
485
486 let res = CapabilityVerifier::new(
488 attenuated.clone(),
489 public_key,
490 "resource1".to_string(),
491 "read".to_string(),
492 )
493 .with_designation("tenant_id".to_string(), "t-123".to_string())
494 .verify();
495 assert!(res.is_err(), "Should fail without namespace");
496 }
497
498 #[test]
499 fn test_builder_issue() {
500 let keypair = KeyPair::new();
501 let public_key = keypair.public();
502
503 let token = HessraCapability::new(
504 "alice".to_string(),
505 "resource1".to_string(),
506 "read".to_string(),
507 TokenTimeConfig::default(),
508 )
509 .issue(&keypair)
510 .expect("Failed to create token");
511
512 let res = CapabilityVerifier::new(
513 token,
514 public_key,
515 "resource1".to_string(),
516 "read".to_string(),
517 )
518 .verify();
519 assert!(res.is_ok());
520 }
521
522 #[test]
523 fn test_custom_time_config() {
524 let root = KeyPair::new();
525 let public_key = root.public();
526
527 let past_time = Utc::now().timestamp() - 3600;
529 let time_config = TokenTimeConfig {
530 start_time: Some(past_time),
531 duration: 7200,
532 };
533
534 let token = HessraCapability::new(
535 "alice".to_string(),
536 "res1".to_string(),
537 "read".to_string(),
538 time_config,
539 )
540 .issue(&root)
541 .expect("Failed to create token");
542
543 let res =
544 CapabilityVerifier::new(token, public_key, "res1".to_string(), "read".to_string())
545 .verify();
546 assert!(res.is_ok());
547 }
548}