hessra_token_core/
classifier.rs

1//! Token classification utilities for analyzing Biscuit token structure
2//!
3//! This module provides functionality to classify and analyze Hessra tokens,
4//! extracting metadata about token type, structure, revocation IDs, and relationships.
5//! This is primarily used for audit logging and building token relationship graphs.
6
7use crate::{
8    revocation::{get_revocation_ids, RevocationId},
9    Biscuit,
10};
11use std::fmt;
12
13/// The type of token
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum TokenType {
16    /// Identity token - represents an identity/principal
17    Identity,
18    /// Authorization token - grants access to a resource
19    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/// The structural pattern of the token
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum TokenStructure {
34    /// Base token with no additional blocks (authority only)
35    Base,
36    /// Token with delegation blocks (identity tokens)
37    Delegated { depth: usize },
38    /// Token with service chain attestations
39    ServiceChain { nodes: usize },
40    /// Token with multi-party attestations
41    MultiParty { parties: usize },
42    /// Token with JIT time attenuation/restriction
43    TimeAttenuated,
44    /// Token with multiple types of blocks
45    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/// The type/role of a specific block in a token
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum BlockType {
64    /// The authority (first) block
65    Authority,
66    /// A delegation block (for identity tokens)
67    Delegation { delegated_identity: String },
68    /// A service chain attestation block
69    ServiceChainAttestation { service: String },
70    /// A multi-party attestation block
71    MultiPartyAttestation { namespace: String },
72    /// A time attenuation/restriction block
73    TimeAttenuation,
74    /// Unknown/other block type
75    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/// Metadata about a specific block in a token
98#[derive(Debug, Clone)]
99pub struct BlockMetadata {
100    /// The index of this block (0 = authority)
101    pub index: usize,
102    /// The revocation ID for this block
103    pub revocation_id: RevocationId,
104    /// The type/role of this block
105    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/// Complete classification of a token
121#[derive(Debug, Clone)]
122pub struct TokenClassification {
123    /// The type of token
124    pub token_type: TokenType,
125    /// The structural pattern
126    pub structure: TokenStructure,
127    /// Metadata for each block
128    pub blocks: Vec<BlockMetadata>,
129    /// Subject/identity from the authority block
130    pub subject: Option<String>,
131    /// Resource (for authorization tokens)
132    pub resource: Option<String>,
133    /// Operation (for authorization tokens)
134    pub operation: Option<String>,
135}
136
137impl TokenClassification {
138    /// Get all revocation IDs from this token
139    pub fn revocation_ids(&self) -> Vec<&RevocationId> {
140        self.blocks.iter().map(|b| &b.revocation_id).collect()
141    }
142
143    /// Get the authority block's revocation ID
144    pub fn authority_revocation_id(&self) -> Option<&RevocationId> {
145        self.blocks.first().map(|b| &b.revocation_id)
146    }
147
148    /// Get the active/current revocation ID (last block)
149    pub fn active_revocation_id(&self) -> Option<&RevocationId> {
150        self.blocks.last().map(|b| &b.revocation_id)
151    }
152
153    /// Get the number of blocks
154    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
181/// Classify a token by analyzing its structure and contents
182///
183/// This function examines a Biscuit token and extracts:
184/// - Token type (identity vs authorization)
185/// - Token structure (base, delegated, service chain, etc.)
186/// - All revocation IDs with their roles
187/// - Subject, resource, operation (as applicable)
188///
189/// # Arguments
190/// * `biscuit` - The Biscuit token to classify
191///
192/// # Returns
193/// * `TokenClassification` - Complete classification of the token
194pub fn classify_token(biscuit: &Biscuit) -> TokenClassification {
195    // Get all revocation IDs
196    let revocation_ids = get_revocation_ids(biscuit);
197
198    // Get the token content for parsing
199    let content = biscuit.print();
200
201    // Determine token type and extract metadata
202    let (token_type, subject, resource, operation) = determine_token_type(&content);
203
204    // Analyze blocks to determine structure and classify each block
205    let blocks = classify_blocks(biscuit, &revocation_ids, &content);
206
207    // Determine the overall structure
208    let structure = determine_structure(&blocks);
209
210    TokenClassification {
211        token_type,
212        structure,
213        blocks,
214        subject,
215        resource,
216        operation,
217    }
218}
219
220/// Determine the token type and extract basic metadata
221fn 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        // Look for identity token markers
234        if trimmed.starts_with("subject(") {
235            is_identity = true;
236            subject = extract_quoted_value(trimmed, "subject(");
237        }
238
239        // Look for authorization token markers
240        if trimmed.starts_with("right(") {
241            is_authorization = true;
242            // right("subject", "resource", "operation")
243            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        // Default to authorization if unclear
257        TokenType::Authorization
258    };
259
260    (token_type, subject, resource, operation)
261}
262
263/// Classify all blocks in the token
264fn classify_blocks(
265    _biscuit: &Biscuit,
266    revocation_ids: &[RevocationId],
267    content: &str,
268) -> Vec<BlockMetadata> {
269    let mut blocks = Vec::new();
270
271    // Parse the authority block (index 0)
272    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    // Parse additional blocks if present
281    if revocation_ids.len() > 1 {
282        // Look for "blocks: [" section in the content
283        if let Some(blocks_start) = content.find("blocks: [") {
284            let blocks_section = &content[blocks_start..];
285
286            // Split into individual block sections
287            let block_strings: Vec<&str> = blocks_section
288                .split("Block {")
289                .skip(1) // Skip the part before the first block
290                .collect();
291
292            for (idx, block_str) in block_strings.iter().enumerate() {
293                let block_index = idx + 1; // +1 because block 0 is authority
294                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
309/// Classify the type of a specific block based on its content
310fn classify_block_type(block_content: &str) -> BlockType {
311    // Look for common patterns in block content
312
313    // Check for delegation (delegated_identity fact)
314    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    // Check for service chain attestation (service fact)
323    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    // Check for multi-party attestation (namespace fact)
330    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    // Check for time attenuation (time checks)
337    if block_content.contains("time(") && block_content.contains("check if") {
338        return BlockType::TimeAttenuation;
339    }
340
341    BlockType::Other
342}
343
344/// Determine the overall token structure based on classified blocks
345fn 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        // Skip authority block
360        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    // Determine structure based on combinations
381    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
412/// Extract a quoted string value from a fact
413fn 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
427/// Extract the three values from a right() fact
428fn 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}