Skip to main content

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    Biscuit,
9    revocation::{RevocationId, get_revocation_ids},
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    /// Capability token - grants access to a resource
19    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/// 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 JIT time attenuation/restriction
39    TimeAttenuated,
40    /// Token with multiple types of blocks
41    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/// The type/role of a specific block in a token
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum BlockType {
58    /// The authority (first) block
59    Authority,
60    /// A delegation block (for identity tokens)
61    Delegation { delegated_identity: String },
62    /// A time attenuation/restriction block
63    TimeAttenuation,
64    /// Unknown/other block type
65    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/// Metadata about a specific block in a token
82#[derive(Debug, Clone)]
83pub struct BlockMetadata {
84    /// The index of this block (0 = authority)
85    pub index: usize,
86    /// The revocation ID for this block
87    pub revocation_id: RevocationId,
88    /// The type/role of this block
89    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/// Complete classification of a token
105#[derive(Debug, Clone)]
106pub struct TokenClassification {
107    /// The type of token
108    pub token_type: TokenType,
109    /// The structural pattern
110    pub structure: TokenStructure,
111    /// Metadata for each block
112    pub blocks: Vec<BlockMetadata>,
113    /// Subject/identity from the authority block
114    pub subject: Option<String>,
115    /// Resource (for capability tokens)
116    pub resource: Option<String>,
117    /// Operation (for capability tokens)
118    pub operation: Option<String>,
119}
120
121impl TokenClassification {
122    /// Get all revocation IDs from this token
123    pub fn revocation_ids(&self) -> Vec<&RevocationId> {
124        self.blocks.iter().map(|b| &b.revocation_id).collect()
125    }
126
127    /// Get the authority block's revocation ID
128    pub fn authority_revocation_id(&self) -> Option<&RevocationId> {
129        self.blocks.first().map(|b| &b.revocation_id)
130    }
131
132    /// Get the active/current revocation ID (last block)
133    pub fn active_revocation_id(&self) -> Option<&RevocationId> {
134        self.blocks.last().map(|b| &b.revocation_id)
135    }
136
137    /// Get the number of blocks
138    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
165/// Classify a token by analyzing its structure and contents
166///
167/// This function examines a Biscuit token and extracts:
168/// - Token type (identity vs capability)
169/// - Token structure (base, delegated, etc.)
170/// - All revocation IDs with their roles
171/// - Subject, resource, operation (as applicable)
172///
173/// # Arguments
174/// * `biscuit` - The Biscuit token to classify
175///
176/// # Returns
177/// * `TokenClassification` - Complete classification of the token
178pub fn classify_token(biscuit: &Biscuit) -> TokenClassification {
179    // Get all revocation IDs
180    let revocation_ids = get_revocation_ids(biscuit);
181
182    // Get the token content for parsing
183    let content = biscuit.print();
184
185    // Determine token type and extract metadata
186    let (token_type, subject, resource, operation) = determine_token_type(&content);
187
188    // Analyze blocks to determine structure and classify each block
189    let blocks = classify_blocks(biscuit, &revocation_ids, &content);
190
191    // Determine the overall structure
192    let structure = determine_structure(&blocks);
193
194    TokenClassification {
195        token_type,
196        structure,
197        blocks,
198        subject,
199        resource,
200        operation,
201    }
202}
203
204/// Determine the token type and extract basic metadata
205fn 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        // Look for identity token markers
218        if trimmed.starts_with("subject(") {
219            is_identity = true;
220            subject = extract_quoted_value(trimmed, "subject(");
221        }
222
223        // Look for capability token markers
224        if trimmed.starts_with("right(") {
225            is_capability = true;
226            // right("subject", "resource", "operation")
227            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        // Default to capability if unclear
241        TokenType::Capability
242    };
243
244    (token_type, subject, resource, operation)
245}
246
247/// Classify all blocks in the token
248fn classify_blocks(
249    _biscuit: &Biscuit,
250    revocation_ids: &[RevocationId],
251    content: &str,
252) -> Vec<BlockMetadata> {
253    let mut blocks = Vec::new();
254
255    // Parse the authority block (index 0)
256    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    // Parse additional blocks if present
265    if revocation_ids.len() > 1 {
266        // Look for "blocks: [" section in the content
267        if let Some(blocks_start) = content.find("blocks: [") {
268            let blocks_section = &content[blocks_start..];
269
270            // Split into individual block sections
271            let block_strings: Vec<&str> = blocks_section
272                .split("Block {")
273                .skip(1) // Skip the part before the first block
274                .collect();
275
276            for (idx, block_str) in block_strings.iter().enumerate() {
277                let block_index = idx + 1; // +1 because block 0 is authority
278                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
293/// Classify the type of a specific block based on its content
294fn classify_block_type(block_content: &str) -> BlockType {
295    // Check for delegation (delegated_identity fact)
296    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    // Check for time attenuation (time checks)
305    if block_content.contains("time(") && block_content.contains("check if") {
306        return BlockType::TimeAttenuation;
307    }
308
309    BlockType::Other
310}
311
312/// Determine the overall token structure based on classified blocks
313fn 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        // Skip authority block
324        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    // Determine structure based on combinations
337    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
355/// Extract a quoted string value from a fact
356fn 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
370/// Extract the three values from a right() fact
371fn 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}