use crate::{
Biscuit,
revocation::{RevocationId, get_revocation_ids},
};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenType {
Identity,
Capability,
}
impl fmt::Display for TokenType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TokenType::Identity => write!(f, "identity"),
TokenType::Capability => write!(f, "capability"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenStructure {
Base,
Delegated { depth: usize },
TimeAttenuated,
Complex,
}
impl fmt::Display for TokenStructure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TokenStructure::Base => write!(f, "base"),
TokenStructure::Delegated { depth } => write!(f, "delegated(depth={depth})"),
TokenStructure::TimeAttenuated => write!(f, "time_attenuated"),
TokenStructure::Complex => write!(f, "complex"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockType {
Authority,
Delegation { delegated_identity: String },
TimeAttenuation,
Other,
}
impl fmt::Display for BlockType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BlockType::Authority => write!(f, "authority"),
BlockType::Delegation { delegated_identity } => {
write!(f, "delegation(identity={delegated_identity})")
}
BlockType::TimeAttenuation => write!(f, "time_attenuation"),
BlockType::Other => write!(f, "other"),
}
}
}
#[derive(Debug, Clone)]
pub struct BlockMetadata {
pub index: usize,
pub revocation_id: RevocationId,
pub block_type: BlockType,
}
impl fmt::Display for BlockMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"block[{}]: {} (revoc_id={})",
self.index,
self.block_type,
self.revocation_id.to_hex()
)
}
}
#[derive(Debug, Clone)]
pub struct TokenClassification {
pub token_type: TokenType,
pub structure: TokenStructure,
pub blocks: Vec<BlockMetadata>,
pub subject: Option<String>,
pub resource: Option<String>,
pub operation: Option<String>,
}
impl TokenClassification {
pub fn revocation_ids(&self) -> Vec<&RevocationId> {
self.blocks.iter().map(|b| &b.revocation_id).collect()
}
pub fn authority_revocation_id(&self) -> Option<&RevocationId> {
self.blocks.first().map(|b| &b.revocation_id)
}
pub fn active_revocation_id(&self) -> Option<&RevocationId> {
self.blocks.last().map(|b| &b.revocation_id)
}
pub fn block_count(&self) -> usize {
self.blocks.len()
}
}
impl fmt::Display for TokenClassification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Token Classification:")?;
writeln!(f, " Type: {}", self.token_type)?;
writeln!(f, " Structure: {}", self.structure)?;
if let Some(subject) = &self.subject {
writeln!(f, " Subject: {subject}")?;
}
if let Some(resource) = &self.resource {
writeln!(f, " Resource: {resource}")?;
}
if let Some(operation) = &self.operation {
writeln!(f, " Operation: {operation}")?;
}
writeln!(f, " Blocks ({}):", self.blocks.len())?;
for block in &self.blocks {
writeln!(f, " {block}")?;
}
Ok(())
}
}
pub fn classify_token(biscuit: &Biscuit) -> TokenClassification {
let revocation_ids = get_revocation_ids(biscuit);
let content = biscuit.print();
let (token_type, subject, resource, operation) = determine_token_type(&content);
let blocks = classify_blocks(biscuit, &revocation_ids, &content);
let structure = determine_structure(&blocks);
TokenClassification {
token_type,
structure,
blocks,
subject,
resource,
operation,
}
}
fn determine_token_type(
content: &str,
) -> (TokenType, Option<String>, Option<String>, Option<String>) {
let mut is_identity = false;
let mut is_capability = false;
let mut subject = None;
let mut resource = None;
let mut operation = None;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("subject(") {
is_identity = true;
subject = extract_quoted_value(trimmed, "subject(");
}
if trimmed.starts_with("right(") {
is_capability = true;
if let Some(values) = extract_right_values(trimmed) {
subject = Some(values.0);
resource = Some(values.1);
operation = Some(values.2);
}
}
}
let token_type = if is_identity {
TokenType::Identity
} else if is_capability {
TokenType::Capability
} else {
TokenType::Capability
};
(token_type, subject, resource, operation)
}
fn classify_blocks(
_biscuit: &Biscuit,
revocation_ids: &[RevocationId],
content: &str,
) -> Vec<BlockMetadata> {
let mut blocks = Vec::new();
if let Some(auth_rev_id) = revocation_ids.first() {
blocks.push(BlockMetadata {
index: 0,
revocation_id: auth_rev_id.clone(),
block_type: BlockType::Authority,
});
}
if revocation_ids.len() > 1 {
if let Some(blocks_start) = content.find("blocks: [") {
let blocks_section = &content[blocks_start..];
let block_strings: Vec<&str> = blocks_section
.split("Block {")
.skip(1) .collect();
for (idx, block_str) in block_strings.iter().enumerate() {
let block_index = idx + 1; if let Some(rev_id) = revocation_ids.get(block_index) {
let block_type = classify_block_type(block_str);
blocks.push(BlockMetadata {
index: block_index,
revocation_id: rev_id.clone(),
block_type,
});
}
}
}
}
blocks
}
fn classify_block_type(block_content: &str) -> BlockType {
if block_content.contains("delegated_identity(")
&& let Some(identity) = extract_quoted_value(block_content, "delegated_identity(")
{
return BlockType::Delegation {
delegated_identity: identity,
};
}
if block_content.contains("time(") && block_content.contains("check if") {
return BlockType::TimeAttenuation;
}
BlockType::Other
}
fn determine_structure(blocks: &[BlockMetadata]) -> TokenStructure {
if blocks.len() == 1 {
return TokenStructure::Base;
}
let mut has_delegation = false;
let mut delegation_count = 0;
let mut has_time_attenuation = false;
for block in blocks.iter().skip(1) {
match &block.block_type {
BlockType::Delegation { .. } => {
has_delegation = true;
delegation_count += 1;
}
BlockType::TimeAttenuation => {
has_time_attenuation = true;
}
_ => {}
}
}
let complexity_count = [has_delegation, has_time_attenuation]
.iter()
.filter(|&&x| x)
.count();
if complexity_count > 1 {
TokenStructure::Complex
} else if has_delegation {
TokenStructure::Delegated {
depth: delegation_count,
}
} else if has_time_attenuation {
TokenStructure::TimeAttenuated
} else {
TokenStructure::Base
}
}
fn extract_quoted_value(line: &str, prefix: &str) -> Option<String> {
if let Some(start_idx) = line.find(prefix) {
let after_prefix = &line[start_idx + prefix.len()..];
if let Some(first_quote) = after_prefix.find('"')
&& let Some(second_quote) = after_prefix[first_quote + 1..].find('"')
{
return Some(after_prefix[first_quote + 1..first_quote + 1 + second_quote].to_string());
}
}
None
}
fn extract_right_values(line: &str) -> Option<(String, String, String)> {
if let Some(start) = line.find("right(") {
let content = &line[start + 6..];
if let Some(end) = content.find(')') {
let values_str = &content[..end];
let values: Vec<&str> = values_str.split(',').map(|s| s.trim()).collect();
if values.len() == 3 {
let subject = values[0].trim_matches('"').to_string();
let resource = values[1].trim_matches('"').to_string();
let operation = values[2].trim_matches('"').to_string();
return Some((subject, resource, operation));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::KeyPair;
use biscuit_auth::macros::biscuit;
#[test]
fn test_classify_base_capability_token() {
let keypair = KeyPair::new();
let biscuit = biscuit!(
r#"
right("alice", "resource1", "read");
"#
)
.build(&keypair)
.unwrap();
let classification = classify_token(&biscuit);
assert_eq!(classification.token_type, TokenType::Capability);
assert_eq!(classification.structure, TokenStructure::Base);
assert_eq!(classification.block_count(), 1);
assert_eq!(classification.subject, Some("alice".to_string()));
assert_eq!(classification.resource, Some("resource1".to_string()));
assert_eq!(classification.operation, Some("read".to_string()));
assert_eq!(classification.blocks[0].block_type, BlockType::Authority);
}
#[test]
fn test_revocation_id_extraction() {
let keypair = KeyPair::new();
let biscuit = biscuit!(
r#"
right("alice", "resource1", "read");
"#
)
.build(&keypair)
.unwrap();
let classification = classify_token(&biscuit);
let auth_id = classification.authority_revocation_id();
assert!(auth_id.is_some());
assert!(!auth_id.unwrap().to_hex().is_empty());
let active_id = classification.active_revocation_id();
assert!(active_id.is_some());
assert_eq!(auth_id, active_id);
}
#[test]
fn test_display_classification() {
let keypair = KeyPair::new();
let biscuit = biscuit!(
r#"
right("alice", "resource1", "read");
"#
)
.build(&keypair)
.unwrap();
let classification = classify_token(&biscuit);
let display_str = format!("{classification}");
assert!(display_str.contains("Token Classification"));
assert!(display_str.contains("Type: capability"));
assert!(display_str.contains("Structure: base"));
assert!(display_str.contains("Subject: alice"));
}
}