use super::super::{AgentHttpAuthenticationError, AgentHttpOperation, AgentHttpPrincipal};
use super::IntrospectionConfigurationError;
use stateknot_core::{PrincipalIdentity, TenantId};
use stateknot_runtime::AgentServiceCaller;
use std::{collections::BTreeMap, sync::RwLock, time::Duration};
use tokio::time::Instant;
#[derive(Clone)]
pub struct TenantBinding {
caller: AgentServiceCaller,
operations: Vec<AgentHttpOperation>,
}
impl TenantBinding {
#[must_use]
pub fn new(
tenant: TenantId,
principal: PrincipalIdentity,
operations: &[AgentHttpOperation],
) -> Self {
let operations = [
AgentHttpOperation::Submit,
AgentHttpOperation::Read,
AgentHttpOperation::Cancel,
AgentHttpOperation::InspectHost,
]
.into_iter()
.filter(|op| operations.contains(op))
.collect();
Self {
caller: AgentServiceCaller::new(tenant, principal),
operations,
}
}
}
struct Snapshot {
generation: u64,
expires: Instant,
bindings: BTreeMap<PrincipalIdentity, TenantBinding>,
}
pub struct TenantPolicy(RwLock<Snapshot>);
impl TenantPolicy {
pub const MAX_BINDINGS: usize = 1024;
pub const MAX_LEASE: Duration = Duration::from_secs(3600);
pub fn new(
bindings: Vec<TenantBinding>,
lease: Duration,
) -> Result<Self, IntrospectionConfigurationError> {
Ok(Self(RwLock::new(Self::snapshot(bindings, lease, 1)?)))
}
pub fn replace(
&self,
expected_generation: u64,
bindings: Vec<TenantBinding>,
lease: Duration,
) -> Result<u64, IntrospectionConfigurationError> {
let next = expected_generation
.checked_add(1)
.ok_or(IntrospectionConfigurationError)?;
let replacement = Self::snapshot(bindings, lease, next)?;
let mut current = self
.0
.write()
.map_err(|_| IntrospectionConfigurationError)?;
if current.generation != expected_generation {
return Err(IntrospectionConfigurationError);
}
*current = replacement;
Ok(next)
}
pub fn generation(&self) -> Result<u64, IntrospectionConfigurationError> {
self.0
.read()
.map(|s| s.generation)
.map_err(|_| IntrospectionConfigurationError)
}
pub(super) fn check(&self) -> Result<(), AgentHttpAuthenticationError> {
let current = self
.0
.read()
.map_err(|_| AgentHttpAuthenticationError::Unavailable)?;
if Instant::now() >= current.expires {
return Err(AgentHttpAuthenticationError::Unavailable);
}
Ok(())
}
pub(super) fn resolve(
&self,
principal: &PrincipalIdentity,
scopes: &[String],
required: &[String; 3],
inspection_scope: Option<&str>,
) -> Result<AgentHttpPrincipal, AgentHttpAuthenticationError> {
let current = self
.0
.read()
.map_err(|_| AgentHttpAuthenticationError::Unavailable)?;
if Instant::now() >= current.expires {
return Err(AgentHttpAuthenticationError::Unavailable);
}
let binding = current
.bindings
.get(principal)
.ok_or(AgentHttpAuthenticationError::Unauthenticated)?;
let operations = [
AgentHttpOperation::Submit,
AgentHttpOperation::Read,
AgentHttpOperation::Cancel,
]
.into_iter()
.zip(required)
.filter_map(|(op, scope)| {
(binding.operations.contains(&op) && scopes.contains(scope)).then_some(op)
});
let inspection = inspection_scope
.filter(|scope| {
binding
.operations
.contains(&AgentHttpOperation::InspectHost)
&& scopes.iter().any(|granted| granted == scope)
})
.map(|_| AgentHttpOperation::InspectHost);
Ok(AgentHttpPrincipal::new(
binding.caller.clone(),
operations.chain(inspection),
))
}
fn snapshot(
bindings: Vec<TenantBinding>,
lease: Duration,
generation: u64,
) -> Result<Snapshot, IntrospectionConfigurationError> {
if bindings.len() > Self::MAX_BINDINGS || lease.is_zero() || lease > Self::MAX_LEASE {
return Err(IntrospectionConfigurationError);
}
let mut result = BTreeMap::new();
for binding in bindings {
if result
.insert(binding.caller.principal().clone(), binding)
.is_some()
{
return Err(IntrospectionConfigurationError);
}
}
Ok(Snapshot {
generation,
expires: Instant::now() + lease,
bindings: result,
})
}
}