use crate::error::TokenError;
use regex::Regex;
use std::sync::OnceLock;
pub fn parse_check_failure(block_id: u32, check_id: u32, rule: &str) -> TokenError {
if let Some(error) = try_parse_expiration(block_id, check_id, rule) {
return error;
}
if let Some(error) = try_parse_identity(block_id, check_id, rule) {
return error;
}
if let Some(error) = try_parse_hierarchy(block_id, check_id, rule) {
return error;
}
TokenError::CheckFailed {
block_id,
check_id,
rule: rule.to_string(),
}
}
fn try_parse_expiration(block_id: u32, check_id: u32, rule: &str) -> Option<TokenError> {
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| Regex::new(r"check if time\(\$\w+\), \$\w+ < (\d+)").unwrap());
if let Some(captures) = re.captures(rule)
&& let Some(timestamp_str) = captures.get(1)
&& let Ok(expired_at) = timestamp_str.as_str().parse::<i64>()
{
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
return Some(TokenError::Expired {
expired_at,
current_time,
block_id,
check_id,
});
}
None
}
fn try_parse_identity(_block_id: u32, _check_id: u32, rule: &str) -> Option<TokenError> {
static RE: OnceLock<Regex> = OnceLock::new();
let re =
RE.get_or_init(|| Regex::new(r#"check if actor\(\$\w+\), \$\w+ == "([^"]+)""#).unwrap());
if let Some(captures) = re.captures(rule)
&& let Some(identity_match) = captures.get(1)
{
let expected = identity_match.as_str().to_string();
return Some(TokenError::IdentityMismatch {
expected,
actual: "<unknown>".to_string(),
});
}
None
}
fn try_parse_hierarchy(block_id: u32, check_id: u32, rule: &str) -> Option<TokenError> {
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
Regex::new(
r#"check if actor\(\$\w+\), \$\w+ == "([^"]+)" \|\| \$\w+\.starts_with\("([^"]+):"\)"#,
)
.unwrap()
});
if let Some(captures) = re.captures(rule)
&& let Some(base_identity_match) = captures.get(1)
{
let expected = base_identity_match.as_str().to_string();
let delegatable = rule.contains("starts_with");
return Some(TokenError::HierarchyViolation {
expected,
actual: "<unknown>".to_string(),
delegatable,
block_id,
check_id,
});
}
None
}
pub fn parse_capability_failure(
subject: Option<&str>,
resource: Option<&str>,
operation: Option<&str>,
rule: &str,
) -> TokenError {
let (parsed_subject, parsed_resource, parsed_operation) = parse_authz_from_rule(rule);
let subject = subject.or(parsed_subject.as_deref());
let resource = resource
.or(parsed_resource.as_deref())
.unwrap_or("<unknown>");
let operation = operation
.or(parsed_operation.as_deref())
.unwrap_or("<unknown>");
TokenError::RightsDenied {
subject: subject.map(|s| s.to_string()),
resource: resource.to_string(),
operation: operation.to_string(),
}
}
fn parse_authz_from_rule(rule: &str) -> (Option<String>, Option<String>, Option<String>) {
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| Regex::new(r#"right\("([^"]+)", "([^"]+)", "([^"]+)"\)"#).unwrap());
if let Some(captures) = re.captures(rule) {
let subject = captures.get(1).map(|m| m.as_str().to_string());
let resource = captures.get(2).map(|m| m.as_str().to_string());
let operation = captures.get(3).map(|m| m.as_str().to_string());
return (subject, resource, operation);
}
(None, None, None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_expiration() {
let rule = "check if time($time), $time < 1735689600";
let error = try_parse_expiration(0, 0, rule);
assert!(error.is_some());
if let Some(TokenError::Expired { expired_at, .. }) = error {
assert_eq!(expired_at, 1735689600);
} else {
panic!("Expected Expired error");
}
}
#[test]
fn test_parse_identity() {
let rule = r#"check if actor($a), $a == "user@example.com""#;
let error = try_parse_identity(0, 0, rule);
assert!(error.is_some());
if let Some(TokenError::IdentityMismatch { expected, .. }) = error {
assert_eq!(expected, "user@example.com");
} else {
panic!("Expected IdentityMismatch error");
}
}
#[test]
fn test_parse_hierarchy() {
let rule = r#"check if actor($a), $a == "base" || $a.starts_with("base:")"#;
let error = try_parse_hierarchy(0, 0, rule);
assert!(error.is_some());
if let Some(TokenError::HierarchyViolation {
expected,
delegatable,
..
}) = error
{
assert_eq!(expected, "base");
assert!(delegatable);
} else {
panic!("Expected HierarchyViolation error");
}
}
#[test]
fn test_parse_authz_from_rule() {
let rule = r#"allow if subject($sub), resource($res), operation($op), right("user@example.com", "/api/data", "read")"#;
let (subject, resource, operation) = parse_authz_from_rule(rule);
assert_eq!(subject, Some("user@example.com".to_string()));
assert_eq!(resource, Some("/api/data".to_string()));
assert_eq!(operation, Some("read".to_string()));
}
#[test]
fn test_parse_check_failure_expiration() {
let rule = "check if time($time), $time < 1735689600";
let error = parse_check_failure(0, 0, rule);
assert!(matches!(error, TokenError::Expired { .. }));
}
#[test]
fn test_parse_check_failure_unknown() {
let rule = "check if some_unknown_check()";
let error = parse_check_failure(0, 0, rule);
assert!(matches!(error, TokenError::CheckFailed { .. }));
}
}