use std::marker::PhantomData;
use typesec_core::policy::{CapabilityError, MintOptions, PolicyEngine, mint_capability_for_id};
use typesec_core::resource::GenericResource;
use typesec_core::{CanDelegate, Capability, SubjectId};
pub trait ConversationState: private::Sealed + Send + Sync + 'static {}
mod private {
pub trait Sealed {}
}
#[derive(Debug)]
pub struct Proposed;
#[derive(Debug)]
pub struct AwaitingConsent;
#[derive(Debug)]
pub struct Consented;
impl private::Sealed for Proposed {}
impl private::Sealed for AwaitingConsent {}
impl private::Sealed for Consented {}
impl ConversationState for Proposed {}
impl ConversationState for AwaitingConsent {}
impl ConversationState for Consented {}
pub struct Conversation<S: ConversationState> {
peer: String,
purpose: Option<String>,
scopes: Vec<String>,
consent: Option<Capability<CanDelegate, GenericResource>>,
_state: PhantomData<fn() -> S>,
}
impl<S: ConversationState> std::fmt::Debug for Conversation<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Conversation")
.field("peer", &self.peer)
.field("purpose", &self.purpose)
.field("scopes", &self.scopes)
.field("state", &std::any::type_name::<S>())
.finish_non_exhaustive()
}
}
impl<S: ConversationState> Conversation<S> {
pub fn peer(&self) -> &str {
&self.peer
}
pub fn purpose(&self) -> Option<&str> {
self.purpose.as_deref()
}
pub fn resource_id(&self) -> String {
format!("conversation/{}", self.peer)
}
fn transition<T: ConversationState>(self) -> Conversation<T> {
Conversation {
peer: self.peer,
purpose: self.purpose,
scopes: self.scopes,
consent: self.consent,
_state: PhantomData,
}
}
}
impl Conversation<Proposed> {
pub fn propose(peer: impl Into<String>) -> Self {
Self {
peer: peer.into(),
purpose: None,
scopes: Vec::new(),
consent: None,
_state: PhantomData,
}
}
#[must_use]
pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
self.purpose = Some(purpose.into());
self
}
pub fn request_consent<I, T>(mut self, scopes: I) -> Conversation<AwaitingConsent>
where
I: IntoIterator<Item = T>,
T: Into<String>,
{
self.scopes = scopes.into_iter().map(Into::into).collect();
self.transition()
}
}
impl Conversation<AwaitingConsent> {
pub fn requested_scopes(&self) -> &[String] {
&self.scopes
}
#[allow(clippy::result_large_err)]
pub fn grant_via(
self,
engine: &dyn PolicyEngine,
subject: impl Into<SubjectId>,
) -> Result<Conversation<Consented>, (Self, CapabilityError)> {
match mint_capability_for_id::<CanDelegate, GenericResource>(
engine,
subject,
self.resource_id(),
&MintOptions::default(),
) {
Ok(consent) => {
let mut conversation = self.transition::<Consented>();
conversation.consent = Some(consent);
Ok(conversation)
}
Err(err) => Err((self, err)),
}
}
}
impl Conversation<Consented> {
pub fn consented_scopes(&self) -> &[String] {
&self.scopes
}
pub fn covers(&self, action: &str) -> bool {
self.scopes.iter().any(|scope| scope == action)
}
pub fn consent(&self) -> &Capability<CanDelegate, GenericResource> {
self.consent
.as_ref()
.expect("Consented state always holds the minted capability")
}
}
#[cfg(test)]
mod tests;