extern crate biscuit_auth as biscuit;
use std::time::Duration;
use biscuit::macros::{biscuit, check};
use chrono::Utc;
use hessra_token_core::{KeyPair, PublicKey, TokenError, TokenTimeConfig};
use tracing::debug;
#[derive(Default)]
pub struct HessraCapability {
subject: Option<String>,
resource: Option<String>,
operation: Option<String>,
designations: Vec<(String, String)>,
designations_trusting: Vec<(PublicKey, String, String)>,
valid_for: Option<Duration>,
latent: Option<PublicKey>,
time_config: TokenTimeConfig,
}
impl HessraCapability {
pub fn new() -> Self {
Self::default()
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn resource(mut self, resource: impl Into<String>) -> Self {
self.resource = Some(resource.into());
self
}
pub fn operation(mut self, operation: impl Into<String>) -> Self {
self.operation = Some(operation.into());
self
}
pub fn designation(mut self, label: impl Into<String>, value: impl Into<String>) -> Self {
self.designations.push((label.into(), value.into()));
self
}
pub fn designation_trusting(
mut self,
pk: PublicKey,
label: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.designations_trusting
.push((pk, label.into(), value.into()));
self
}
pub fn anchor(self, anchor: impl Into<String>) -> Self {
self.designation("anchor", anchor)
}
pub fn valid_for(mut self, duration: Duration) -> Self {
self.valid_for = Some(duration);
self
}
pub fn latent(mut self, activator_pk: PublicKey) -> Self {
self.latent = Some(activator_pk);
self
}
pub fn with_time(mut self, time_config: TokenTimeConfig) -> Self {
self.time_config = time_config;
self
}
pub fn issue(self, keypair: &KeyPair) -> Result<String, TokenError> {
let subject = self
.subject
.ok_or_else(|| TokenError::generic("capability requires a subject"))?;
let resource = self
.resource
.ok_or_else(|| TokenError::generic("capability requires a resource"))?;
let operation = self
.operation
.ok_or_else(|| TokenError::generic("capability requires an operation"))?;
let now = self
.time_config
.start_time
.unwrap_or_else(|| Utc::now().timestamp());
let valid_for = self
.valid_for
.map(|d| d.as_secs() as i64)
.unwrap_or(self.time_config.duration);
let expiration = now + valid_for;
let mut builder = biscuit!(
r#"
right({subject}, {resource}, {operation});
check if resource($res), operation($op), right($sub, $res, $op);
check if time($time), $time < {expiration};
"#
);
for (label, value) in &self.designations {
let (label, value) = (label.clone(), value.clone());
builder = builder.check(check!(r#"check if designation({label}, {value});"#))?;
}
for (pk, label, value) in &self.designations_trusting {
let (pk, label, value) = (*pk, label.clone(), value.clone());
builder = builder.check(check!(
r#"check if designation({label}, {value}) trusting {pk};"#
))?;
}
if let Some(activator_pk) = self.latent {
builder = builder.check(check!(
r#"check if activation($a) trusting {activator_pk};"#
))?;
}
let biscuit = builder.build(keypair)?;
debug!("minted capability authority block: {}", biscuit);
Ok(biscuit.to_base64()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::verify::CapabilityVerifier;
use std::time::Duration;
fn issue(authority: &KeyPair) -> String {
HessraCapability::new()
.subject("alice")
.resource("res1")
.operation("read")
.issue(authority)
.expect("issue")
}
#[test]
fn create_and_verify() {
let root = KeyPair::new();
let token = issue(&root);
assert!(
CapabilityVerifier::new(token, root.public(), "res1".into(), "read".into())
.verify()
.is_ok()
);
}
#[test]
fn wrong_resource_or_operation_rejected() {
let root = KeyPair::new();
let token = issue(&root);
assert!(
CapabilityVerifier::new(token.clone(), root.public(), "res2".into(), "read".into())
.verify()
.is_err()
);
assert!(
CapabilityVerifier::new(token, root.public(), "res1".into(), "write".into())
.verify()
.is_err()
);
}
#[test]
fn missing_field_errors() {
let root = KeyPair::new();
assert!(
HessraCapability::new()
.resource("r")
.operation("o")
.issue(&root)
.is_err()
);
}
#[test]
fn fresh_cap_within_window_passes() {
let root = KeyPair::new();
let token = HessraCapability::new()
.subject("alice")
.resource("res1")
.operation("read")
.valid_for(Duration::from_secs(60))
.issue(&root)
.unwrap();
assert!(
CapabilityVerifier::new(token, root.public(), "res1".into(), "read".into())
.verify()
.is_ok()
);
}
#[test]
fn expired_cap_rejected() {
use hessra_token_core::TokenTimeConfig;
let root = KeyPair::new();
let base = 1_000_000_000;
let token = HessraCapability::new()
.subject("alice")
.resource("res1")
.operation("read")
.with_time(TokenTimeConfig {
start_time: Some(base),
duration: 300,
})
.issue(&root)
.unwrap();
assert!(
CapabilityVerifier::new(token, root.public(), "res1".into(), "read".into())
.with_time(TokenTimeConfig {
start_time: Some(base + 301),
duration: 300,
})
.verify()
.is_err()
);
}
#[test]
fn anchor_binds_to_one_verifier() {
let root = KeyPair::new();
let token = HessraCapability::new()
.subject("alice")
.resource("res1")
.operation("read")
.anchor("webapp")
.issue(&root)
.unwrap();
assert!(
CapabilityVerifier::new(token.clone(), root.public(), "res1".into(), "read".into())
.anchor("webapp")
.verify()
.is_ok()
);
assert!(
CapabilityVerifier::new(token.clone(), root.public(), "res1".into(), "read".into())
.verify()
.is_err()
);
assert!(
CapabilityVerifier::new(token, root.public(), "res1".into(), "read".into())
.anchor("bobapp")
.verify()
.is_err()
);
}
}