extern crate biscuit_auth as biscuit;
use biscuit::macros::{biscuit, check};
use chrono::Utc;
use hessra_token_core::{KeyPair, TokenTimeConfig};
use std::error::Error;
use tracing::info;
pub struct HessraCapability {
subject: Option<String>,
resource: Option<String>,
operation: Option<String>,
time_config: TokenTimeConfig,
anchor: Option<String>,
}
impl HessraCapability {
pub fn new(
subject: String,
resource: String,
operation: String,
time_config: TokenTimeConfig,
) -> Self {
Self {
subject: Some(subject),
resource: Some(resource),
operation: Some(operation),
time_config,
anchor: None,
}
}
pub fn anchor_bound(mut self, anchor: String) -> Self {
self.anchor = Some(anchor);
self
}
pub fn issue(self, keypair: &KeyPair) -> Result<String, Box<dyn Error>> {
let start_time = self
.time_config
.start_time
.unwrap_or_else(|| Utc::now().timestamp());
let expiration = start_time + self.time_config.duration;
let anchor = self.anchor;
let subject = self.subject.ok_or("Token requires subject")?;
let resource = self.resource.ok_or("Token requires resource")?;
let operation = self.operation.ok_or("Token requires operation")?;
let mut biscuit_builder = biscuit!(
r#"
right({subject}, {resource}, {operation});
check if resource($res), operation($op), right($sub, $res, $op);
check if time($time), $time < {expiration};
"#
);
if let Some(anchor) = anchor {
let anchor_label = "anchor".to_string();
biscuit_builder = biscuit_builder.check(check!(
r#"
check if designation({anchor_label}, {anchor});
"#
))?;
}
let biscuit = biscuit_builder.build(keypair)?;
info!("biscuit (authority): {}", biscuit);
let token = biscuit.to_base64()?;
Ok(token)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::verify::CapabilityVerifier;
use chrono::Utc;
#[test]
fn test_create_and_verify_capability() {
let subject = "test@test.com".to_owned();
let resource = "res1".to_string();
let operation = "read".to_string();
let root = KeyPair::new();
let public_key = root.public();
let token = HessraCapability::new(
subject.clone(),
resource.clone(),
operation.clone(),
TokenTimeConfig::default(),
)
.issue(&root)
.expect("Failed to create token");
let res = CapabilityVerifier::new(token, public_key, resource, operation).verify();
assert!(res.is_ok());
}
#[test]
fn test_capability_without_subject() {
let subject = "alice".to_owned();
let resource = "res1".to_string();
let operation = "read".to_string();
let root = KeyPair::new();
let public_key = root.public();
let token = HessraCapability::new(
subject.clone(),
resource.clone(),
operation.clone(),
TokenTimeConfig::default(),
)
.issue(&root)
.expect("Failed to create token");
let res = CapabilityVerifier::new(token, public_key, resource, operation).verify();
assert!(
res.is_ok(),
"Capability verification without subject should succeed"
);
}
#[test]
fn test_capability_with_optional_subject() {
let subject = "alice".to_owned();
let resource = "res1".to_string();
let operation = "read".to_string();
let root = KeyPair::new();
let public_key = root.public();
let token = HessraCapability::new(
subject.clone(),
resource.clone(),
operation.clone(),
TokenTimeConfig::default(),
)
.issue(&root)
.expect("Failed to create token");
let res = CapabilityVerifier::new(
token.clone(),
public_key,
resource.clone(),
operation.clone(),
)
.with_subject(subject.clone())
.verify();
assert!(
res.is_ok(),
"Verification with correct subject should succeed"
);
let res = CapabilityVerifier::new(
token.clone(),
public_key,
resource.clone(),
operation.clone(),
)
.with_subject("bob".to_string())
.verify();
assert!(res.is_err(), "Verification with wrong subject should fail");
}
#[test]
fn test_wrong_resource_rejected() {
let root = KeyPair::new();
let public_key = root.public();
let token = HessraCapability::new(
"alice".to_string(),
"res1".to_string(),
"read".to_string(),
TokenTimeConfig::default(),
)
.issue(&root)
.expect("Failed to create token");
let res =
CapabilityVerifier::new(token, public_key, "res2".to_string(), "read".to_string())
.verify();
assert!(res.is_err(), "Wrong resource should be rejected");
}
#[test]
fn test_wrong_operation_rejected() {
let root = KeyPair::new();
let public_key = root.public();
let token = HessraCapability::new(
"alice".to_string(),
"res1".to_string(),
"read".to_string(),
TokenTimeConfig::default(),
)
.issue(&root)
.expect("Failed to create token");
let res =
CapabilityVerifier::new(token, public_key, "res1".to_string(), "write".to_string())
.verify();
assert!(res.is_err(), "Wrong operation should be rejected");
}
#[test]
fn test_biscuit_expiration() {
let subject = "test@test.com".to_owned();
let resource = "res1".to_string();
let operation = "read".to_string();
let root = KeyPair::new();
let public_key = root.public();
let token = HessraCapability::new(
subject.clone(),
resource.clone(),
operation.clone(),
TokenTimeConfig::default(),
)
.issue(&root)
.expect("Failed to create token");
let res = CapabilityVerifier::new(token, public_key, resource.clone(), operation.clone())
.verify();
assert!(res.is_ok());
let root = KeyPair::new();
let public_key = root.public();
let token = HessraCapability::new(
subject.clone(),
resource.clone(),
operation.clone(),
TokenTimeConfig {
start_time: Some(Utc::now().timestamp() - 301),
duration: 300,
},
)
.issue(&root)
.expect("Failed to create expired token");
let res = CapabilityVerifier::new(token, public_key, resource, operation).verify();
assert!(res.is_err(), "Expired token should be rejected");
}
#[test]
fn test_anchor_bound_capability() {
let keypair = KeyPair::new();
let public_key = keypair.public();
let token = HessraCapability::new(
"alice".to_string(),
"resource1".to_string(),
"read".to_string(),
TokenTimeConfig::default(),
)
.anchor_bound("webapp".to_string())
.issue(&keypair)
.expect("Failed to create anchor-bound token");
let res = CapabilityVerifier::new(
token.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("anchor".to_string(), "webapp".to_string())
.verify();
assert!(res.is_ok(), "Verification at the anchor should succeed");
let res = CapabilityVerifier::new(
token.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.verify();
assert!(
res.is_err(),
"Should fail when verifier does not assert any anchor"
);
let res = CapabilityVerifier::new(
token.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("anchor".to_string(), "bobapp".to_string())
.verify();
assert!(
res.is_err(),
"Should fail when verifier's claimed principal is not the anchor"
);
}
#[test]
fn test_anchor_survives_attenuation() {
let keypair = KeyPair::new();
let public_key = keypair.public();
let token = HessraCapability::new(
"alice".to_string(),
"resource1".to_string(),
"read".to_string(),
TokenTimeConfig::default(),
)
.anchor_bound("webapp".to_string())
.issue(&keypair)
.expect("Failed to create token");
let attenuated = crate::attenuate::DesignationBuilder::from_base64(token, public_key)
.expect("Failed to create designation builder")
.designate("user".to_string(), "alice".to_string())
.attenuate_base64()
.expect("Failed to attenuate");
let res = CapabilityVerifier::new(
attenuated.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("anchor".to_string(), "webapp".to_string())
.with_designation("user".to_string(), "alice".to_string())
.verify();
assert!(
res.is_ok(),
"Verifier at the anchor should succeed with all designations"
);
let res = CapabilityVerifier::new(
attenuated,
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("user".to_string(), "alice".to_string())
.verify();
assert!(
res.is_err(),
"Anchor check survives attenuation, still required at verify"
);
}
#[test]
fn test_designation_attenuation() {
let keypair = KeyPair::new();
let public_key = keypair.public();
let token = HessraCapability::new(
"alice".to_string(),
"resource1".to_string(),
"read".to_string(),
TokenTimeConfig::default(),
)
.issue(&keypair)
.expect("Failed to create token");
let attenuated = crate::attenuate::DesignationBuilder::from_base64(token, public_key)
.expect("Failed to create designation builder")
.designate("tenant_id".to_string(), "t-123".to_string())
.attenuate_base64()
.expect("Failed to attenuate");
let res = CapabilityVerifier::new(
attenuated.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("tenant_id".to_string(), "t-123".to_string())
.verify();
assert!(res.is_ok(), "Should pass with matching designation");
let res = CapabilityVerifier::new(
attenuated.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("tenant_id".to_string(), "t-999".to_string())
.verify();
assert!(res.is_err(), "Should fail with wrong designation value");
let res = CapabilityVerifier::new(
attenuated.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.verify();
assert!(res.is_err(), "Should fail without designation");
}
#[test]
fn test_multi_designation() {
let keypair = KeyPair::new();
let public_key = keypair.public();
let token = HessraCapability::new(
"alice".to_string(),
"resource1".to_string(),
"read".to_string(),
TokenTimeConfig::default(),
)
.issue(&keypair)
.expect("Failed to create token");
let attenuated = crate::attenuate::DesignationBuilder::from_base64(token, public_key)
.expect("Failed to create designation builder")
.designate("tenant_id".to_string(), "t-123".to_string())
.designate("user_id".to_string(), "u-456".to_string())
.attenuate_base64()
.expect("Failed to attenuate");
let res = CapabilityVerifier::new(
attenuated.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("tenant_id".to_string(), "t-123".to_string())
.with_designation("user_id".to_string(), "u-456".to_string())
.verify();
assert!(res.is_ok(), "Should pass with both designations");
let res = CapabilityVerifier::new(
attenuated.clone(),
public_key,
"resource1".to_string(),
"read".to_string(),
)
.with_designation("tenant_id".to_string(), "t-123".to_string())
.verify();
assert!(res.is_err(), "Should fail with missing designation");
}
#[test]
fn test_builder_issue() {
let keypair = KeyPair::new();
let public_key = keypair.public();
let token = HessraCapability::new(
"alice".to_string(),
"resource1".to_string(),
"read".to_string(),
TokenTimeConfig::default(),
)
.issue(&keypair)
.expect("Failed to create token");
let res = CapabilityVerifier::new(
token,
public_key,
"resource1".to_string(),
"read".to_string(),
)
.verify();
assert!(res.is_ok());
}
#[test]
fn test_custom_time_config() {
let root = KeyPair::new();
let public_key = root.public();
let past_time = Utc::now().timestamp() - 3600;
let time_config = TokenTimeConfig {
start_time: Some(past_time),
duration: 7200,
};
let token = HessraCapability::new(
"alice".to_string(),
"res1".to_string(),
"read".to_string(),
time_config,
)
.issue(&root)
.expect("Failed to create token");
let res =
CapabilityVerifier::new(token, public_key, "res1".to_string(), "read".to_string())
.verify();
assert!(res.is_ok());
}
}