use crate::canonical::{CanonicalAuthError, validate_scope_label};
use ic_auth_protocol_types::{AuthRole, DelegatedRoleGrant};
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Debug, Error, Eq, PartialEq)]
pub enum SessionRequestError {
#[error("session request has invalid scope bounds, duplicate scopes or zero TTL")]
InvalidRequest,
#[error("invalid session TTL policy, requested lifetime or expiry overflow")]
InvalidLifetime,
#[error("requested scopes are not granted for the protected receiver role")]
ScopeRejected,
#[error(transparent)]
Canonical(#[from] CanonicalAuthError),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SessionTtlLimit(u64);
impl SessionTtlLimit {
pub fn new(seconds: u64) -> Result<Self, SessionRequestError> {
if seconds == 0 || seconds > crate::limits::MAX_SESSION_SECS {
return Err(SessionRequestError::InvalidLifetime);
}
Ok(Self(seconds))
}
pub const fn seconds(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SessionTtlPolicy {
default_secs: u64,
maximum: SessionTtlLimit,
}
impl SessionTtlPolicy {
pub fn new(default_secs: u64, maximum_secs: u64) -> Result<Self, SessionRequestError> {
let maximum = SessionTtlLimit::new(maximum_secs)?;
if default_secs == 0 || default_secs > maximum_secs {
return Err(SessionRequestError::InvalidLifetime);
}
Ok(Self {
default_secs,
maximum,
})
}
pub const fn default_secs(self) -> u64 {
self.default_secs
}
pub const fn maximum(self) -> SessionTtlLimit {
self.maximum
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SessionRequest {
scopes: Vec<String>,
ttl_secs: Option<u64>,
}
impl SessionRequest {
pub fn new(
mut scopes: Vec<String>,
ttl_secs: Option<u64>,
) -> Result<Self, SessionRequestError> {
check_scopes(&scopes)?;
scopes.sort_unstable();
if scopes.windows(2).any(|pair| pair[0] == pair[1]) || ttl_secs == Some(0) {
return Err(SessionRequestError::InvalidRequest);
}
Ok(Self { scopes, ttl_secs })
}
pub const fn ttl_secs(&self) -> Option<u64> {
self.ttl_secs
}
pub fn scopes(&self) -> &[String] {
&self.scopes
}
pub fn expires_at_ns(
&self,
now_ns: u64,
policy: SessionTtlPolicy,
) -> Result<u64, SessionRequestError> {
let ttl = self.ttl_secs.unwrap_or(policy.default_secs());
if ttl > policy.maximum().seconds() {
return Err(SessionRequestError::InvalidLifetime);
}
now_ns
.checked_add(ttl * crate::limits::NS_PER_SECOND)
.ok_or(SessionRequestError::InvalidLifetime)
}
fn check_granted_scopes(&self, granted: &[String]) -> Result<(), SessionRequestError> {
if !crate::certificate_issuance::scopes_are_subset(&self.scopes, granted) {
return Err(SessionRequestError::ScopeRejected);
}
Ok(())
}
pub(crate) fn evaluate(
&self,
grants: &[DelegatedRoleGrant],
role: &AuthRole,
now_ns: u64,
policy: SessionTtlPolicy,
) -> Result<u64, SessionRequestError> {
let grant = grants
.iter()
.find(|grant| &grant.target == role)
.ok_or(SessionRequestError::ScopeRejected)?;
self.check_granted_scopes(&grant.scopes)?;
self.expires_at_ns(now_ns, policy)
}
pub fn hash(&self) -> [u8; 32] {
let mut hash = Sha256::new();
hash.update(b"canic-application-session-request-v1");
hash.update((self.scopes.len() as u64).to_be_bytes());
for scope in &self.scopes {
hash.update((scope.len() as u64).to_be_bytes());
hash.update(scope.as_bytes());
}
match self.ttl_secs {
None => hash.update([0]),
Some(ttl) => {
hash.update([1]);
hash.update(ttl.to_be_bytes());
}
}
hash.finalize().into()
}
}
pub(crate) fn check_scopes(scopes: &[String]) -> Result<(), SessionRequestError> {
if scopes.is_empty() || scopes.len() > crate::limits::MAX_SESSION_SCOPES {
return Err(SessionRequestError::InvalidRequest);
}
for scope in scopes {
validate_scope_label(scope)?;
}
Ok(())
}