typesec_agent/
conversation.rs1use std::marker::PhantomData;
24
25use typesec_core::policy::{CapabilityError, MintOptions, PolicyEngine, mint_capability_for_id};
26use typesec_core::resource::GenericResource;
27use typesec_core::{CanDelegate, Capability, SubjectId};
28
29pub trait ConversationState: private::Sealed + Send + Sync + 'static {}
31
32mod private {
33 pub trait Sealed {}
34}
35
36#[derive(Debug)]
38pub struct Proposed;
39
40#[derive(Debug)]
42pub struct AwaitingConsent;
43
44#[derive(Debug)]
46pub struct Consented;
47
48impl private::Sealed for Proposed {}
49impl private::Sealed for AwaitingConsent {}
50impl private::Sealed for Consented {}
51impl ConversationState for Proposed {}
52impl ConversationState for AwaitingConsent {}
53impl ConversationState for Consented {}
54
55pub struct Conversation<S: ConversationState> {
57 peer: String,
58 purpose: Option<String>,
59 scopes: Vec<String>,
60 consent: Option<Capability<CanDelegate, GenericResource>>,
61 _state: PhantomData<fn() -> S>,
62}
63
64impl<S: ConversationState> std::fmt::Debug for Conversation<S> {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("Conversation")
67 .field("peer", &self.peer)
68 .field("purpose", &self.purpose)
69 .field("scopes", &self.scopes)
70 .field("state", &std::any::type_name::<S>())
71 .finish_non_exhaustive()
72 }
73}
74
75impl<S: ConversationState> Conversation<S> {
76 pub fn peer(&self) -> &str {
78 &self.peer
79 }
80
81 pub fn purpose(&self) -> Option<&str> {
83 self.purpose.as_deref()
84 }
85
86 pub fn resource_id(&self) -> String {
88 format!("conversation/{}", self.peer)
89 }
90
91 fn transition<T: ConversationState>(self) -> Conversation<T> {
92 Conversation {
93 peer: self.peer,
94 purpose: self.purpose,
95 scopes: self.scopes,
96 consent: self.consent,
97 _state: PhantomData,
98 }
99 }
100}
101
102impl Conversation<Proposed> {
103 pub fn propose(peer: impl Into<String>) -> Self {
105 Self {
106 peer: peer.into(),
107 purpose: None,
108 scopes: Vec::new(),
109 consent: None,
110 _state: PhantomData,
111 }
112 }
113
114 #[must_use]
116 pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
117 self.purpose = Some(purpose.into());
118 self
119 }
120
121 pub fn request_consent<I, T>(mut self, scopes: I) -> Conversation<AwaitingConsent>
124 where
125 I: IntoIterator<Item = T>,
126 T: Into<String>,
127 {
128 self.scopes = scopes.into_iter().map(Into::into).collect();
129 self.transition()
130 }
131}
132
133impl Conversation<AwaitingConsent> {
134 pub fn requested_scopes(&self) -> &[String] {
136 &self.scopes
137 }
138
139 #[allow(clippy::result_large_err)]
148 pub fn grant_via(
149 self,
150 engine: &dyn PolicyEngine,
151 subject: impl Into<SubjectId>,
152 ) -> Result<Conversation<Consented>, (Self, CapabilityError)> {
153 match mint_capability_for_id::<CanDelegate, GenericResource>(
154 engine,
155 subject,
156 self.resource_id(),
157 &MintOptions::default(),
158 ) {
159 Ok(consent) => {
160 let mut conversation = self.transition::<Consented>();
161 conversation.consent = Some(consent);
162 Ok(conversation)
163 }
164 Err(err) => Err((self, err)),
165 }
166 }
167}
168
169impl Conversation<Consented> {
170 pub fn consented_scopes(&self) -> &[String] {
172 &self.scopes
173 }
174
175 pub fn covers(&self, action: &str) -> bool {
177 self.scopes.iter().any(|scope| scope == action)
178 }
179
180 pub fn consent(&self) -> &Capability<CanDelegate, GenericResource> {
182 self.consent
183 .as_ref()
184 .expect("Consented state always holds the minted capability")
185 }
186}
187
188#[cfg(test)]
189mod tests;