1use crate::{
8 revocation::{get_revocation_ids, RevocationId},
9 Biscuit,
10};
11use std::fmt;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum TokenType {
16 Identity,
18 Authorization,
20}
21
22impl fmt::Display for TokenType {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 TokenType::Identity => write!(f, "identity"),
26 TokenType::Authorization => write!(f, "authorization"),
27 }
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum TokenStructure {
34 Base,
36 Delegated { depth: usize },
38 ServiceChain { nodes: usize },
40 MultiParty { parties: usize },
42 TimeAttenuated,
44 Complex,
46}
47
48impl fmt::Display for TokenStructure {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 TokenStructure::Base => write!(f, "base"),
52 TokenStructure::Delegated { depth } => write!(f, "delegated(depth={depth})"),
53 TokenStructure::ServiceChain { nodes } => write!(f, "service_chain(nodes={nodes})"),
54 TokenStructure::MultiParty { parties } => write!(f, "multi_party(parties={parties})"),
55 TokenStructure::TimeAttenuated => write!(f, "time_attenuated"),
56 TokenStructure::Complex => write!(f, "complex"),
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum BlockType {
64 Authority,
66 Delegation { delegated_identity: String },
68 ServiceChainAttestation { service: String },
70 MultiPartyAttestation { namespace: String },
72 TimeAttenuation,
74 Other,
76}
77
78impl fmt::Display for BlockType {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 BlockType::Authority => write!(f, "authority"),
82 BlockType::Delegation { delegated_identity } => {
83 write!(f, "delegation(identity={delegated_identity})")
84 }
85 BlockType::ServiceChainAttestation { service } => {
86 write!(f, "service_chain_attestation(service={service})")
87 }
88 BlockType::MultiPartyAttestation { namespace } => {
89 write!(f, "multi_party_attestation(namespace={namespace})")
90 }
91 BlockType::TimeAttenuation => write!(f, "time_attenuation"),
92 BlockType::Other => write!(f, "other"),
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct BlockMetadata {
100 pub index: usize,
102 pub revocation_id: RevocationId,
104 pub block_type: BlockType,
106}
107
108impl fmt::Display for BlockMetadata {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(
111 f,
112 "block[{}]: {} (revoc_id={})",
113 self.index,
114 self.block_type,
115 self.revocation_id.to_hex()
116 )
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct TokenClassification {
123 pub token_type: TokenType,
125 pub structure: TokenStructure,
127 pub blocks: Vec<BlockMetadata>,
129 pub subject: Option<String>,
131 pub resource: Option<String>,
133 pub operation: Option<String>,
135}
136
137impl TokenClassification {
138 pub fn revocation_ids(&self) -> Vec<&RevocationId> {
140 self.blocks.iter().map(|b| &b.revocation_id).collect()
141 }
142
143 pub fn authority_revocation_id(&self) -> Option<&RevocationId> {
145 self.blocks.first().map(|b| &b.revocation_id)
146 }
147
148 pub fn active_revocation_id(&self) -> Option<&RevocationId> {
150 self.blocks.last().map(|b| &b.revocation_id)
151 }
152
153 pub fn block_count(&self) -> usize {
155 self.blocks.len()
156 }
157}
158
159impl fmt::Display for TokenClassification {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 writeln!(f, "Token Classification:")?;
162 writeln!(f, " Type: {}", self.token_type)?;
163 writeln!(f, " Structure: {}", self.structure)?;
164 if let Some(subject) = &self.subject {
165 writeln!(f, " Subject: {subject}")?;
166 }
167 if let Some(resource) = &self.resource {
168 writeln!(f, " Resource: {resource}")?;
169 }
170 if let Some(operation) = &self.operation {
171 writeln!(f, " Operation: {operation}")?;
172 }
173 writeln!(f, " Blocks ({}):", self.blocks.len())?;
174 for block in &self.blocks {
175 writeln!(f, " {block}")?;
176 }
177 Ok(())
178 }
179}
180
181pub fn classify_token(biscuit: &Biscuit) -> TokenClassification {
195 let revocation_ids = get_revocation_ids(biscuit);
197
198 let content = biscuit.print();
200
201 let (token_type, subject, resource, operation) = determine_token_type(&content);
203
204 let blocks = classify_blocks(biscuit, &revocation_ids, &content);
206
207 let structure = determine_structure(&blocks);
209
210 TokenClassification {
211 token_type,
212 structure,
213 blocks,
214 subject,
215 resource,
216 operation,
217 }
218}
219
220fn determine_token_type(
222 content: &str,
223) -> (TokenType, Option<String>, Option<String>, Option<String>) {
224 let mut is_identity = false;
225 let mut is_authorization = false;
226 let mut subject = None;
227 let mut resource = None;
228 let mut operation = None;
229
230 for line in content.lines() {
231 let trimmed = line.trim();
232
233 if trimmed.starts_with("subject(") {
235 is_identity = true;
236 subject = extract_quoted_value(trimmed, "subject(");
237 }
238
239 if trimmed.starts_with("right(") {
241 is_authorization = true;
242 if let Some(values) = extract_right_values(trimmed) {
244 subject = Some(values.0);
245 resource = Some(values.1);
246 operation = Some(values.2);
247 }
248 }
249 }
250
251 let token_type = if is_identity {
252 TokenType::Identity
253 } else if is_authorization {
254 TokenType::Authorization
255 } else {
256 TokenType::Authorization
258 };
259
260 (token_type, subject, resource, operation)
261}
262
263fn classify_blocks(
265 _biscuit: &Biscuit,
266 revocation_ids: &[RevocationId],
267 content: &str,
268) -> Vec<BlockMetadata> {
269 let mut blocks = Vec::new();
270
271 if let Some(auth_rev_id) = revocation_ids.first() {
273 blocks.push(BlockMetadata {
274 index: 0,
275 revocation_id: auth_rev_id.clone(),
276 block_type: BlockType::Authority,
277 });
278 }
279
280 if revocation_ids.len() > 1 {
282 if let Some(blocks_start) = content.find("blocks: [") {
284 let blocks_section = &content[blocks_start..];
285
286 let block_strings: Vec<&str> = blocks_section
288 .split("Block {")
289 .skip(1) .collect();
291
292 for (idx, block_str) in block_strings.iter().enumerate() {
293 let block_index = idx + 1; if let Some(rev_id) = revocation_ids.get(block_index) {
295 let block_type = classify_block_type(block_str);
296 blocks.push(BlockMetadata {
297 index: block_index,
298 revocation_id: rev_id.clone(),
299 block_type,
300 });
301 }
302 }
303 }
304 }
305
306 blocks
307}
308
309fn classify_block_type(block_content: &str) -> BlockType {
311 if block_content.contains("delegated_identity(") {
315 if let Some(identity) = extract_quoted_value(block_content, "delegated_identity(") {
316 return BlockType::Delegation {
317 delegated_identity: identity,
318 };
319 }
320 }
321
322 if block_content.contains("service(") {
324 if let Some(service) = extract_quoted_value(block_content, "service(") {
325 return BlockType::ServiceChainAttestation { service };
326 }
327 }
328
329 if block_content.contains("namespace(") {
331 if let Some(namespace) = extract_quoted_value(block_content, "namespace(") {
332 return BlockType::MultiPartyAttestation { namespace };
333 }
334 }
335
336 if block_content.contains("time(") && block_content.contains("check if") {
338 return BlockType::TimeAttenuation;
339 }
340
341 BlockType::Other
342}
343
344fn determine_structure(blocks: &[BlockMetadata]) -> TokenStructure {
346 if blocks.len() == 1 {
347 return TokenStructure::Base;
348 }
349
350 let mut has_delegation = false;
351 let mut delegation_count = 0;
352 let mut has_service_chain = false;
353 let mut service_chain_count = 0;
354 let mut has_multi_party = false;
355 let mut multi_party_count = 0;
356 let mut has_time_attenuation = false;
357
358 for block in blocks.iter().skip(1) {
359 match &block.block_type {
361 BlockType::Delegation { .. } => {
362 has_delegation = true;
363 delegation_count += 1;
364 }
365 BlockType::ServiceChainAttestation { .. } => {
366 has_service_chain = true;
367 service_chain_count += 1;
368 }
369 BlockType::MultiPartyAttestation { .. } => {
370 has_multi_party = true;
371 multi_party_count += 1;
372 }
373 BlockType::TimeAttenuation => {
374 has_time_attenuation = true;
375 }
376 _ => {}
377 }
378 }
379
380 let complexity_count = [
382 has_delegation,
383 has_service_chain,
384 has_multi_party,
385 has_time_attenuation,
386 ]
387 .iter()
388 .filter(|&&x| x)
389 .count();
390
391 if complexity_count > 1 {
392 TokenStructure::Complex
393 } else if has_delegation {
394 TokenStructure::Delegated {
395 depth: delegation_count,
396 }
397 } else if has_service_chain {
398 TokenStructure::ServiceChain {
399 nodes: service_chain_count,
400 }
401 } else if has_multi_party {
402 TokenStructure::MultiParty {
403 parties: multi_party_count,
404 }
405 } else if has_time_attenuation {
406 TokenStructure::TimeAttenuated
407 } else {
408 TokenStructure::Base
409 }
410}
411
412fn extract_quoted_value(line: &str, prefix: &str) -> Option<String> {
414 if let Some(start_idx) = line.find(prefix) {
415 let after_prefix = &line[start_idx + prefix.len()..];
416 if let Some(first_quote) = after_prefix.find('"') {
417 if let Some(second_quote) = after_prefix[first_quote + 1..].find('"') {
418 return Some(
419 after_prefix[first_quote + 1..first_quote + 1 + second_quote].to_string(),
420 );
421 }
422 }
423 }
424 None
425}
426
427fn extract_right_values(line: &str) -> Option<(String, String, String)> {
429 if let Some(start) = line.find("right(") {
430 let content = &line[start + 6..];
431 if let Some(end) = content.find(')') {
432 let values_str = &content[..end];
433 let values: Vec<&str> = values_str.split(',').map(|s| s.trim()).collect();
434
435 if values.len() == 3 {
436 let subject = values[0].trim_matches('"').to_string();
437 let resource = values[1].trim_matches('"').to_string();
438 let operation = values[2].trim_matches('"').to_string();
439 return Some((subject, resource, operation));
440 }
441 }
442 }
443 None
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use crate::KeyPair;
450 use biscuit_auth::macros::biscuit;
451
452 #[test]
453 fn test_classify_base_authorization_token() {
454 let keypair = KeyPair::new();
455
456 let biscuit = biscuit!(
457 r#"
458 right("alice", "resource1", "read");
459 "#
460 )
461 .build(&keypair)
462 .unwrap();
463
464 let classification = classify_token(&biscuit);
465
466 assert_eq!(classification.token_type, TokenType::Authorization);
467 assert_eq!(classification.structure, TokenStructure::Base);
468 assert_eq!(classification.block_count(), 1);
469 assert_eq!(classification.subject, Some("alice".to_string()));
470 assert_eq!(classification.resource, Some("resource1".to_string()));
471 assert_eq!(classification.operation, Some("read".to_string()));
472
473 assert_eq!(classification.blocks[0].block_type, BlockType::Authority);
474 }
475
476 #[test]
477 fn test_revocation_id_extraction() {
478 let keypair = KeyPair::new();
479
480 let biscuit = biscuit!(
481 r#"
482 right("alice", "resource1", "read");
483 "#
484 )
485 .build(&keypair)
486 .unwrap();
487
488 let classification = classify_token(&biscuit);
489
490 let auth_id = classification.authority_revocation_id();
491 assert!(auth_id.is_some());
492 assert!(!auth_id.unwrap().to_hex().is_empty());
493
494 let active_id = classification.active_revocation_id();
495 assert!(active_id.is_some());
496 assert_eq!(auth_id, active_id);
497 }
498
499 #[test]
500 fn test_display_classification() {
501 let keypair = KeyPair::new();
502
503 let biscuit = biscuit!(
504 r#"
505 right("alice", "resource1", "read");
506 "#
507 )
508 .build(&keypair)
509 .unwrap();
510
511 let classification = classify_token(&biscuit);
512 let display_str = format!("{}", classification);
513
514 assert!(display_str.contains("Token Classification"));
515 assert!(display_str.contains("Type: authorization"));
516 assert!(display_str.contains("Structure: base"));
517 assert!(display_str.contains("Subject: alice"));
518 }
519}