use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum TokenError {
#[error("Invalid token signature: {details}")]
InvalidSignature { details: String },
#[error("Unknown public key - token was not signed by a recognized authority")]
UnknownPublicKey,
#[error("Invalid key format: {reason}")]
InvalidKeyFormat { reason: String },
#[error("Failed to deserialize token: {reason}")]
DeserializationError { reason: String },
#[error("Failed to serialize token: {reason}")]
SerializationError { reason: String },
#[error("Failed to decode base64 token: {reason}")]
Base64DecodingError { reason: String },
#[error("Unsupported token version: expected {maximum}-{minimum}, got {actual}")]
UnsupportedVersion {
maximum: u32,
minimum: u32,
actual: u32,
},
#[error("Token expired at {expired_at}, current time is {current_time}")]
Expired {
expired_at: i64,
current_time: i64,
block_id: u32,
check_id: u32,
},
#[error("Verification check failed in block {block_id}, check {check_id}: {rule}")]
CheckFailed {
block_id: u32,
check_id: u32,
rule: String,
},
#[error("Identity mismatch: expected '{expected}', got '{actual}'")]
IdentityMismatch {
expected: String,
actual: String,
},
#[error(
"Identity hierarchy violation: actor '{actual}' is not authorized for identity '{expected}' (delegatable: {delegatable})"
)]
HierarchyViolation {
expected: String,
actual: String,
delegatable: bool,
block_id: u32,
check_id: u32,
},
#[error("Token attenuation failed: {reason}")]
AttenuationFailed { reason: String },
#[error("Bearer token not allowed: {reason}")]
BearerNotAllowed { reason: String },
#[error("Authorization denied: {subject} does not have permission to perform '{operation}' on '{resource}'", subject = subject.as_deref().unwrap_or("<capability bearer>"))]
RightsDenied {
subject: Option<String>,
resource: String,
operation: String,
},
#[error("No matching authorization policy found. Failed checks: {}", format_check_failures(.failed_checks))]
NoMatchingPolicy {
failed_checks: Vec<CheckFailure>,
},
#[error("Authorization policy {policy_type} matched (index {policy_index}), but the following checks failed: {}", format_check_failures(.failed_checks))]
PolicyMatchedButChecksFailed {
policy_type: String,
policy_index: usize,
failed_checks: Vec<CheckFailure>,
},
#[error("Token verification exceeded execution limits: {reason}")]
ExecutionLimitReached { reason: String },
#[error("Token verification expression error: {reason}")]
ExpressionError { reason: String },
#[error("Invalid rule in block {block_id}: {rule}")]
InvalidBlockRule { block_id: u32, rule: String },
#[error("Internal error: {0}")]
Internal(String),
#[error("{0}")]
Generic(String),
}
#[derive(Debug, Clone)]
pub struct CheckFailure {
pub block_id: Option<u32>,
pub check_id: u32,
pub rule: String,
}
impl std::fmt::Display for CheckFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(block_id) = self.block_id {
write!(
f,
"Check #{} in block #{}: {}",
self.check_id, block_id, self.rule
)
} else {
write!(f, "Check #{} in authorizer: {}", self.check_id, self.rule)
}
}
}
fn format_check_failures(checks: &[CheckFailure]) -> String {
checks
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("; ")
}
impl TokenError {
pub fn is_expired(&self) -> bool {
matches!(self, TokenError::Expired { .. })
}
pub fn is_identity_mismatch(&self) -> bool {
matches!(
self,
TokenError::IdentityMismatch { .. } | TokenError::HierarchyViolation { .. }
)
}
pub fn is_rights_denied(&self) -> bool {
matches!(self, TokenError::RightsDenied { .. })
}
pub fn is_signature_error(&self) -> bool {
matches!(
self,
TokenError::InvalidSignature { .. }
| TokenError::UnknownPublicKey
| TokenError::DeserializationError { .. }
| TokenError::Base64DecodingError { .. }
)
}
pub fn get_expiration_time(&self) -> Option<i64> {
match self {
TokenError::Expired { expired_at, .. } => Some(*expired_at),
_ => None,
}
}
pub fn get_denied_access(&self) -> Option<(Option<&str>, &str, &str)> {
match self {
TokenError::RightsDenied {
subject,
resource,
operation,
} => Some((subject.as_deref(), resource.as_str(), operation.as_str())),
_ => None,
}
}
pub fn generic<S: Into<String>>(msg: S) -> Self {
TokenError::Generic(msg.into())
}
pub fn internal<S: Into<String>>(msg: S) -> Self {
TokenError::Internal(msg.into())
}
pub fn invalid_key_format<S: Into<String>>(reason: S) -> Self {
TokenError::InvalidKeyFormat {
reason: reason.into(),
}
}
}
impl From<biscuit_auth::error::Token> for TokenError {
fn from(err: biscuit_auth::error::Token) -> Self {
use biscuit_auth::error::{Logic, MatchedPolicy, Token};
match err {
Token::Format(format_err) => {
use biscuit_auth::error::Format;
match format_err {
Format::Signature(sig_err) => TokenError::InvalidSignature {
details: sig_err.to_string(),
},
Format::UnknownPublicKey => TokenError::UnknownPublicKey,
Format::DeserializationError(msg) | Format::BlockDeserializationError(msg) => {
TokenError::DeserializationError { reason: msg }
}
Format::SerializationError(msg) | Format::BlockSerializationError(msg) => {
TokenError::SerializationError { reason: msg }
}
Format::Version {
maximum,
minimum,
actual,
} => TokenError::UnsupportedVersion {
maximum,
minimum,
actual,
},
Format::InvalidKey(msg) => TokenError::InvalidKeyFormat { reason: msg },
other => TokenError::Generic(other.to_string()),
}
}
Token::Base64(base64_err) => TokenError::Base64DecodingError {
reason: base64_err.to_string(),
},
Token::FailedLogic(logic_err) => match logic_err {
Logic::Unauthorized { policy, checks } => {
let failed_checks = convert_failed_checks(checks);
let (policy_type, policy_index) = match policy {
MatchedPolicy::Allow(idx) => ("Allow", idx),
MatchedPolicy::Deny(idx) => ("Deny", idx),
};
TokenError::PolicyMatchedButChecksFailed {
policy_type: policy_type.to_string(),
policy_index,
failed_checks,
}
}
Logic::NoMatchingPolicy { checks } => {
let failed_checks = convert_failed_checks(checks);
TokenError::NoMatchingPolicy { failed_checks }
}
Logic::InvalidBlockRule(block_id, rule) => {
TokenError::InvalidBlockRule { block_id, rule }
}
Logic::AuthorizerNotEmpty => {
TokenError::Internal("Authorizer already contains a token".to_string())
}
},
Token::RunLimit(limit) => TokenError::ExecutionLimitReached {
reason: limit.to_string(),
},
Token::Execution(expr) => TokenError::ExpressionError {
reason: expr.to_string(),
},
Token::Language(lang_err) => TokenError::Generic(lang_err.to_string()),
other => TokenError::Generic(other.to_string()),
}
}
}
fn convert_failed_checks(checks: Vec<biscuit_auth::error::FailedCheck>) -> Vec<CheckFailure> {
checks
.into_iter()
.map(|check| match check {
biscuit_auth::error::FailedCheck::Block(block_check) => CheckFailure {
block_id: Some(block_check.block_id),
check_id: block_check.check_id,
rule: block_check.rule,
},
biscuit_auth::error::FailedCheck::Authorizer(auth_check) => CheckFailure {
block_id: None,
check_id: auth_check.check_id,
rule: auth_check.rule,
},
})
.collect()
}
impl From<hex::FromHexError> for TokenError {
fn from(err: hex::FromHexError) -> Self {
TokenError::InvalidKeyFormat {
reason: err.to_string(),
}
}
}
impl From<&str> for TokenError {
fn from(err: &str) -> Self {
TokenError::Generic(err.to_string())
}
}
impl From<String> for TokenError {
fn from(err: String) -> Self {
TokenError::Generic(err)
}
}