extern crate biscuit_auth as biscuit;
use biscuit::Algorithm;
use biscuit::macros::{authorizer, check, fact};
use chrono::Utc;
use hessra_token_core::{
Biscuit, PublicKey, TokenError, TokenTimeConfig, parse_capability_failure, parse_check_failure,
};
pub struct CapabilityVerifier {
token: String,
public_key: PublicKey,
resource: String,
operation: String,
subject: Option<String>,
designations: Vec<(String, String)>,
time_config: TokenTimeConfig,
}
impl CapabilityVerifier {
pub fn new(token: String, public_key: PublicKey, resource: String, operation: String) -> Self {
Self {
token,
public_key,
resource,
operation,
subject: None,
designations: Vec::new(),
time_config: TokenTimeConfig::default(),
}
}
pub fn with_subject(mut self, subject: String) -> Self {
self.subject = Some(subject);
self
}
pub fn with_designation(mut self, label: String, value: String) -> Self {
self.designations.push((label, value));
self
}
pub fn anchor(self, anchor: impl Into<String>) -> Self {
self.with_designation("anchor".to_string(), anchor.into())
}
pub fn with_time(mut self, time_config: TokenTimeConfig) -> Self {
self.time_config = time_config;
self
}
pub fn verify(self) -> Result<(), TokenError> {
let biscuit = Biscuit::from_base64(&self.token, self.public_key)?;
let now = self
.time_config
.start_time
.unwrap_or_else(|| Utc::now().timestamp());
let resource = self.resource.clone();
let operation = self.operation.clone();
let mut authz = authorizer!(
r#"
time({now});
resource({resource});
operation({operation});
allow if true;
"#
);
if let Some(ref subject) = self.subject {
let subject = subject.clone();
let resource = self.resource.clone();
let operation = self.operation.clone();
authz = authz.check(check!(
r#"check if right({subject}, {resource}, {operation});"#
))?;
}
for (label, value) in &self.designations {
let label = label.clone();
let value = value.clone();
authz = authz.fact(fact!(r#"designation({label}, {value});"#))?;
}
let limits = biscuit::datalog::RunLimits {
max_time: std::time::Duration::from_millis(50),
..Default::default()
};
match authz.build(&biscuit)?.authorize_with_limits(limits) {
Ok(_) => Ok(()),
Err(e) => Err(convert_capability_error(
e,
self.subject.as_deref(),
Some(&self.resource),
Some(&self.operation),
)),
}
}
}
pub fn biscuit_key_from_string(key: String) -> Result<PublicKey, TokenError> {
let parts = key.split('/').collect::<Vec<&str>>();
if parts.len() != 2 {
return Err(TokenError::invalid_key_format(
"Key must be in format 'algorithm/hexkey'",
));
}
let alg = match parts[0] {
"ed25519" => Algorithm::Ed25519,
"secp256r1" => Algorithm::Secp256r1,
_ => {
return Err(TokenError::invalid_key_format(
"Unsupported algorithm, must be ed25519 or secp256r1",
));
}
};
let key_bytes = hex::decode(parts[1])?;
let key = PublicKey::from_bytes(&key_bytes, alg)
.map_err(|e| TokenError::invalid_key_format(e.to_string()))?;
Ok(key)
}
fn convert_capability_error(
err: biscuit::error::Token,
subject: Option<&str>,
resource: Option<&str>,
operation: Option<&str>,
) -> TokenError {
use biscuit::error::{Logic, Token};
match err {
Token::FailedLogic(logic_err) => match &logic_err {
Logic::Unauthorized { checks, .. } | Logic::NoMatchingPolicy { checks } => {
for failed_check in checks.iter() {
let (block_id, check_id, rule) = match failed_check {
biscuit::error::FailedCheck::Block(block_check) => (
block_check.block_id,
block_check.check_id,
block_check.rule.clone(),
),
biscuit::error::FailedCheck::Authorizer(auth_check) => {
(0, auth_check.check_id, auth_check.rule.clone())
}
};
let parsed_error = parse_check_failure(block_id, check_id, &rule);
if matches!(parsed_error, TokenError::Expired { .. }) {
return parsed_error;
}
}
if matches!(logic_err, Logic::NoMatchingPolicy { .. }) {
return parse_capability_failure(
subject,
resource,
operation,
&format!("{checks:?}"),
);
}
TokenError::from(Token::FailedLogic(logic_err))
}
other => TokenError::from(Token::FailedLogic(other.clone())),
},
other => TokenError::from(other),
}
}