use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, error::Error as StdError};
use thiserror::Error;
use crate::{
Context, DecisionAuditOccurrence, PolicyAnchor, RequestId, SubjectRef, TenantBinding, TenantId,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttemptFailure {
Resolution,
Evidence,
Trace,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "AttemptWire")]
pub struct AuthorizationAttempt {
schema_version: u16,
occurrence: DecisionAuditOccurrence,
anchor: PolicyAnchor,
tenant: TenantId,
binding: TenantBinding,
principal: SubjectRef,
subjects: BTreeMap<crate::SubjectSlot, SubjectRef>,
request_id: Option<RequestId>,
failure: AttemptFailure,
}
#[derive(Deserialize)]
struct AttemptWire {
schema_version: u16,
occurrence: DecisionAuditOccurrence,
anchor: PolicyAnchor,
tenant: TenantId,
binding: TenantBinding,
principal: SubjectRef,
subjects: BTreeMap<crate::SubjectSlot, SubjectRef>,
request_id: Option<RequestId>,
failure: AttemptFailure,
}
impl TryFrom<AttemptWire> for AuthorizationAttempt {
type Error = AttemptValidationError;
fn try_from(wire: AttemptWire) -> Result<Self, Self::Error> {
let entry = Self {
schema_version: wire.schema_version,
occurrence: wire.occurrence,
anchor: wire.anchor,
tenant: wire.tenant,
binding: wire.binding,
principal: wire.principal,
subjects: wire.subjects,
request_id: wire.request_id,
failure: wire.failure,
};
entry.validate()?;
Ok(entry)
}
}
impl AuthorizationAttempt {
pub fn new(
context: &Context,
anchor: PolicyAnchor,
failure: AttemptFailure,
occurrence: DecisionAuditOccurrence,
now: time::OffsetDateTime,
) -> Result<Self, AttemptValidationError> {
context.validate_at(now)?;
let entry = Self {
schema_version: 1,
occurrence,
anchor,
tenant: context.tenant().clone(),
binding: context.binding().clone(),
principal: context.principal().clone(),
subjects: context.subjects().clone(),
request_id: context.request_id().cloned(),
failure,
};
entry.validate()?;
Ok(entry)
}
pub fn validate(&self) -> Result<(), AttemptValidationError> {
if self.schema_version != 1 {
return Err(AttemptValidationError::Schema);
}
if self.tenant != *self.binding.tenant() {
return Err(AttemptValidationError::Tenant);
}
self.occurrence.validate()?;
Ok(())
}
#[must_use]
pub const fn schema_version(&self) -> u16 {
self.schema_version
}
#[must_use]
pub const fn binding(&self) -> &TenantBinding {
&self.binding
}
#[must_use]
pub const fn principal(&self) -> &SubjectRef {
&self.principal
}
#[must_use]
pub const fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
#[must_use]
pub const fn tenant(&self) -> &TenantId {
&self.tenant
}
#[must_use]
pub const fn occurrence(&self) -> &DecisionAuditOccurrence {
&self.occurrence
}
#[must_use]
pub const fn failure(&self) -> AttemptFailure {
self.failure
}
#[must_use]
pub const fn subjects(&self) -> &BTreeMap<crate::SubjectSlot, SubjectRef> {
&self.subjects
}
#[must_use]
pub const fn anchor(&self) -> &PolicyAnchor {
&self.anchor
}
}
#[derive(Debug, Error)]
pub enum AttemptValidationError {
#[error(transparent)]
Context(#[from] crate::ContextError),
#[error("unsupported attempt schema")]
Schema,
#[error("attempt tenant disagrees with binding")]
Tenant,
#[error(transparent)]
Occurrence(#[from] crate::DecisionAuditOccurrenceError),
}
#[async_trait]
pub trait AttemptAuditSink: Send + Sync {
type Error: StdError + Send + Sync + 'static;
async fn record_attempt(&self, entry: &AuthorizationAttempt) -> Result<(), Self::Error>;
}
#[derive(Debug, Error)]
pub enum AttemptAuthorizationError<R, A, T> {
#[error("authorization failed")]
Authorization(#[source] crate::AuthorizationError<R, A>),
#[error("authorization failed without a valid audit scope")]
Unscoped {
authorization: crate::AuthorizationError<R, A>,
scope: AttemptValidationError,
},
#[error("required authorization-attempt audit failed")]
Persistence {
authorization: crate::AuthorizationError<R, A>,
entry: Box<AuthorizationAttempt>,
#[source]
source: T,
},
}