1use crate::{
8 Biscuit,
9 revocation::{RevocationId, get_revocation_ids},
10};
11use std::fmt;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum TokenType {
16 Identity,
18 Capability,
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::Capability => write!(f, "capability"),
27 }
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum TokenStructure {
34 Base,
36 Delegated { depth: usize },
38 TimeAttenuated,
40 Complex,
42}
43
44impl fmt::Display for TokenStructure {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 TokenStructure::Base => write!(f, "base"),
48 TokenStructure::Delegated { depth } => write!(f, "delegated(depth={depth})"),
49 TokenStructure::TimeAttenuated => write!(f, "time_attenuated"),
50 TokenStructure::Complex => write!(f, "complex"),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum BlockType {
58 Authority,
60 Delegation { delegated_identity: String },
62 TimeAttenuation,
64 Other,
66}
67
68impl fmt::Display for BlockType {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 BlockType::Authority => write!(f, "authority"),
72 BlockType::Delegation { delegated_identity } => {
73 write!(f, "delegation(identity={delegated_identity})")
74 }
75 BlockType::TimeAttenuation => write!(f, "time_attenuation"),
76 BlockType::Other => write!(f, "other"),
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct BlockMetadata {
84 pub index: usize,
86 pub revocation_id: RevocationId,
88 pub block_type: BlockType,
90}
91
92impl fmt::Display for BlockMetadata {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(
95 f,
96 "block[{}]: {} (revoc_id={})",
97 self.index,
98 self.block_type,
99 self.revocation_id.to_hex()
100 )
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct TokenClassification {
107 pub token_type: TokenType,
109 pub structure: TokenStructure,
111 pub blocks: Vec<BlockMetadata>,
113 pub subject: Option<String>,
115 pub resource: Option<String>,
117 pub operation: Option<String>,
119}
120
121impl TokenClassification {
122 pub fn revocation_ids(&self) -> Vec<&RevocationId> {
124 self.blocks.iter().map(|b| &b.revocation_id).collect()
125 }
126
127 pub fn authority_revocation_id(&self) -> Option<&RevocationId> {
129 self.blocks.first().map(|b| &b.revocation_id)
130 }
131
132 pub fn active_revocation_id(&self) -> Option<&RevocationId> {
134 self.blocks.last().map(|b| &b.revocation_id)
135 }
136
137 pub fn block_count(&self) -> usize {
139 self.blocks.len()
140 }
141}
142
143impl fmt::Display for TokenClassification {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 writeln!(f, "Token Classification:")?;
146 writeln!(f, " Type: {}", self.token_type)?;
147 writeln!(f, " Structure: {}", self.structure)?;
148 if let Some(subject) = &self.subject {
149 writeln!(f, " Subject: {subject}")?;
150 }
151 if let Some(resource) = &self.resource {
152 writeln!(f, " Resource: {resource}")?;
153 }
154 if let Some(operation) = &self.operation {
155 writeln!(f, " Operation: {operation}")?;
156 }
157 writeln!(f, " Blocks ({}):", self.blocks.len())?;
158 for block in &self.blocks {
159 writeln!(f, " {block}")?;
160 }
161 Ok(())
162 }
163}
164
165pub fn classify_token(biscuit: &Biscuit) -> TokenClassification {
179 let revocation_ids = get_revocation_ids(biscuit);
181
182 let content = biscuit.print();
184
185 let (token_type, subject, resource, operation) = determine_token_type(&content);
187
188 let blocks = classify_blocks(biscuit, &revocation_ids, &content);
190
191 let structure = determine_structure(&blocks);
193
194 TokenClassification {
195 token_type,
196 structure,
197 blocks,
198 subject,
199 resource,
200 operation,
201 }
202}
203
204fn determine_token_type(
206 content: &str,
207) -> (TokenType, Option<String>, Option<String>, Option<String>) {
208 let mut is_identity = false;
209 let mut is_capability = false;
210 let mut subject = None;
211 let mut resource = None;
212 let mut operation = None;
213
214 for line in content.lines() {
215 let trimmed = line.trim();
216
217 if trimmed.starts_with("subject(") {
219 is_identity = true;
220 subject = extract_quoted_value(trimmed, "subject(");
221 }
222
223 if trimmed.starts_with("right(") {
225 is_capability = true;
226 if let Some(values) = extract_right_values(trimmed) {
228 subject = Some(values.0);
229 resource = Some(values.1);
230 operation = Some(values.2);
231 }
232 }
233 }
234
235 let token_type = if is_identity {
236 TokenType::Identity
237 } else if is_capability {
238 TokenType::Capability
239 } else {
240 TokenType::Capability
242 };
243
244 (token_type, subject, resource, operation)
245}
246
247fn classify_blocks(
249 _biscuit: &Biscuit,
250 revocation_ids: &[RevocationId],
251 content: &str,
252) -> Vec<BlockMetadata> {
253 let mut blocks = Vec::new();
254
255 if let Some(auth_rev_id) = revocation_ids.first() {
257 blocks.push(BlockMetadata {
258 index: 0,
259 revocation_id: auth_rev_id.clone(),
260 block_type: BlockType::Authority,
261 });
262 }
263
264 if revocation_ids.len() > 1 {
266 if let Some(blocks_start) = content.find("blocks: [") {
268 let blocks_section = &content[blocks_start..];
269
270 let block_strings: Vec<&str> = blocks_section
272 .split("Block {")
273 .skip(1) .collect();
275
276 for (idx, block_str) in block_strings.iter().enumerate() {
277 let block_index = idx + 1; if let Some(rev_id) = revocation_ids.get(block_index) {
279 let block_type = classify_block_type(block_str);
280 blocks.push(BlockMetadata {
281 index: block_index,
282 revocation_id: rev_id.clone(),
283 block_type,
284 });
285 }
286 }
287 }
288 }
289
290 blocks
291}
292
293fn classify_block_type(block_content: &str) -> BlockType {
295 if block_content.contains("delegated_identity(") {
297 if let Some(identity) = extract_quoted_value(block_content, "delegated_identity(") {
298 return BlockType::Delegation {
299 delegated_identity: identity,
300 };
301 }
302 }
303
304 if block_content.contains("time(") && block_content.contains("check if") {
306 return BlockType::TimeAttenuation;
307 }
308
309 BlockType::Other
310}
311
312fn determine_structure(blocks: &[BlockMetadata]) -> TokenStructure {
314 if blocks.len() == 1 {
315 return TokenStructure::Base;
316 }
317
318 let mut has_delegation = false;
319 let mut delegation_count = 0;
320 let mut has_time_attenuation = false;
321
322 for block in blocks.iter().skip(1) {
323 match &block.block_type {
325 BlockType::Delegation { .. } => {
326 has_delegation = true;
327 delegation_count += 1;
328 }
329 BlockType::TimeAttenuation => {
330 has_time_attenuation = true;
331 }
332 _ => {}
333 }
334 }
335
336 let complexity_count = [has_delegation, has_time_attenuation]
338 .iter()
339 .filter(|&&x| x)
340 .count();
341
342 if complexity_count > 1 {
343 TokenStructure::Complex
344 } else if has_delegation {
345 TokenStructure::Delegated {
346 depth: delegation_count,
347 }
348 } else if has_time_attenuation {
349 TokenStructure::TimeAttenuated
350 } else {
351 TokenStructure::Base
352 }
353}
354
355fn extract_quoted_value(line: &str, prefix: &str) -> Option<String> {
357 if let Some(start_idx) = line.find(prefix) {
358 let after_prefix = &line[start_idx + prefix.len()..];
359 if let Some(first_quote) = after_prefix.find('"') {
360 if let Some(second_quote) = after_prefix[first_quote + 1..].find('"') {
361 return Some(
362 after_prefix[first_quote + 1..first_quote + 1 + second_quote].to_string(),
363 );
364 }
365 }
366 }
367 None
368}
369
370fn extract_right_values(line: &str) -> Option<(String, String, String)> {
372 if let Some(start) = line.find("right(") {
373 let content = &line[start + 6..];
374 if let Some(end) = content.find(')') {
375 let values_str = &content[..end];
376 let values: Vec<&str> = values_str.split(',').map(|s| s.trim()).collect();
377
378 if values.len() == 3 {
379 let subject = values[0].trim_matches('"').to_string();
380 let resource = values[1].trim_matches('"').to_string();
381 let operation = values[2].trim_matches('"').to_string();
382 return Some((subject, resource, operation));
383 }
384 }
385 }
386 None
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use crate::KeyPair;
393 use biscuit_auth::macros::biscuit;
394
395 #[test]
396 fn test_classify_base_capability_token() {
397 let keypair = KeyPair::new();
398
399 let biscuit = biscuit!(
400 r#"
401 right("alice", "resource1", "read");
402 "#
403 )
404 .build(&keypair)
405 .unwrap();
406
407 let classification = classify_token(&biscuit);
408
409 assert_eq!(classification.token_type, TokenType::Capability);
410 assert_eq!(classification.structure, TokenStructure::Base);
411 assert_eq!(classification.block_count(), 1);
412 assert_eq!(classification.subject, Some("alice".to_string()));
413 assert_eq!(classification.resource, Some("resource1".to_string()));
414 assert_eq!(classification.operation, Some("read".to_string()));
415
416 assert_eq!(classification.blocks[0].block_type, BlockType::Authority);
417 }
418
419 #[test]
420 fn test_revocation_id_extraction() {
421 let keypair = KeyPair::new();
422
423 let biscuit = biscuit!(
424 r#"
425 right("alice", "resource1", "read");
426 "#
427 )
428 .build(&keypair)
429 .unwrap();
430
431 let classification = classify_token(&biscuit);
432
433 let auth_id = classification.authority_revocation_id();
434 assert!(auth_id.is_some());
435 assert!(!auth_id.unwrap().to_hex().is_empty());
436
437 let active_id = classification.active_revocation_id();
438 assert!(active_id.is_some());
439 assert_eq!(auth_id, active_id);
440 }
441
442 #[test]
443 fn test_display_classification() {
444 let keypair = KeyPair::new();
445
446 let biscuit = biscuit!(
447 r#"
448 right("alice", "resource1", "read");
449 "#
450 )
451 .build(&keypair)
452 .unwrap();
453
454 let classification = classify_token(&biscuit);
455 let display_str = format!("{classification}");
456
457 assert!(display_str.contains("Token Classification"));
458 assert!(display_str.contains("Type: capability"));
459 assert!(display_str.contains("Structure: base"));
460 assert!(display_str.contains("Subject: alice"));
461 }
462}