1use std::{fmt, rc::Rc};
3use futures::future::LocalBoxFuture;
4use lenso_kernel::{InvocationContext, ModuleDependencies, RuntimeFailure, NativeRequestEndpoint, NativeRequestHandle, RequestCapability};
5
6pub const CAPABILITY_ID: &str = "lenso.auth@1";
7pub const DESCRIPTOR_VERSION: &str = "1.0.0";
8pub const PORTABLE: bool = true;
9pub const CROSS_LANE_TRANSFER: bool = false;
10pub const AUTH_CAPABILITY_ID: &str = CAPABILITY_ID;
11pub const AUTH_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;
12
13pub const AUTHENTICATE_OPERATION: &str = "authenticate";
14
15pub type Int64 = String;
16pub type Uint64 = String;
17pub type Bytes = String;
18pub type Timestamp = String;
19pub type Duration = String;
20pub type OptionalValue<T> = Option<Option<T>>;
21
22#[allow(dead_code)]
23fn deserialize_required<'de, D, T>(deserializer: D) -> Result<T, D::Error>
24where
25 D: serde::Deserializer<'de>,
26 T: serde::Deserialize<'de>,
27{
28 <T as serde::Deserialize>::deserialize(deserializer)
29}
30
31#[allow(dead_code, clippy::option_option)]
32fn deserialize_optional_value<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
33where
34 D: serde::Deserializer<'de>,
35 T: serde::Deserialize<'de>,
36{
37 Ok(Some(<Option<T> as serde::Deserialize>::deserialize(deserializer)?))
38}
39
40#[allow(dead_code)]
41fn validate_portable_json_value(value: &serde_json::Value) -> Result<(), String> {
42 match value {
43 serde_json::Value::Number(number) => {
44 let safe = number.as_i64().is_some_and(|value| (-9_007_199_254_740_991..=9_007_199_254_740_991).contains(&value))
45 || number.as_u64().is_some_and(|value| value <= 9_007_199_254_740_991)
46 || (number.is_f64() && number.as_f64().is_some_and(|value| value.is_finite() && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)));
47 if !safe {
48 return Err("wire JSON contains an unsafe number".to_owned());
49 }
50 }
51 serde_json::Value::Array(values) => {
52 for value in values {
53 validate_portable_json_value(value)?;
54 }
55 }
56 serde_json::Value::Object(values) => {
57 for value in values.values() {
58 validate_portable_json_value(value)?;
59 }
60 }
61 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => {}
62 }
63 Ok(())
64}
65
66#[allow(dead_code)]
67fn portable_json_error(detail: String) -> serde_json::Error {
68 serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidData, detail))
69}
70
71#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
72pub struct UnknownDomainError {
73 pub code: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub payload: Option<serde_json::Value>,
76 #[serde(default, flatten)]
77 pub extra: std::collections::BTreeMap<String, serde_json::Value>,
78}
79
80#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
81pub struct AuthenticateRequest {
82 #[serde(rename = "credential")]
83 #[serde(deserialize_with = "deserialize_required")]
84 pub credential: Option<AuthenticateRequestCredential>,
85}
86
87#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
88pub struct AuthenticateRequestCredential {
89 #[serde(rename = "scheme")]
90 #[serde(deserialize_with = "deserialize_required")]
91 pub scheme: String,
92 #[serde(rename = "value")]
93 #[serde(deserialize_with = "deserialize_required")]
94 pub value: String,
95}
96
97impl fmt::Debug for AuthenticateRequestCredential {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 formatter
100 .debug_struct("AuthenticateRequestCredential")
101 .field("scheme", &self.scheme)
102 .field("value", &"<redacted>")
103 .finish()
104 }
105}
106
107#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
108pub struct AuthenticateResponse {
109 #[serde(rename = "assertion")]
110 #[serde(deserialize_with = "deserialize_required")]
111 pub assertion: Option<AuthenticateResponseAssertion>,
112 #[serde(rename = "kind")]
113 #[serde(deserialize_with = "deserialize_required")]
114 pub kind: AuthenticateResponseKind,
115}
116
117#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
118pub struct AuthenticateResponseAssertion {
119 #[serde(rename = "actor_kind")]
120 #[serde(deserialize_with = "deserialize_required")]
121 pub actor_kind: String,
122 #[serde(rename = "assurance")]
123 #[serde(deserialize_with = "deserialize_required")]
124 pub assurance: String,
125 #[serde(rename = "audience")]
126 #[serde(deserialize_with = "deserialize_required")]
127 pub audience: Vec<String>,
128 #[serde(rename = "claims")]
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub claims: Option<std::collections::BTreeMap<String, serde_json::Value>>,
131 #[serde(rename = "expires_at")]
132 #[serde(deserialize_with = "deserialize_required")]
133 pub expires_at: Timestamp,
134 #[serde(rename = "issued_at")]
135 #[serde(deserialize_with = "deserialize_required")]
136 pub issued_at: Timestamp,
137 #[serde(rename = "issuer")]
138 #[serde(deserialize_with = "deserialize_required")]
139 pub issuer: String,
140 #[serde(rename = "parent_provenance")]
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub parent_provenance: Option<String>,
143 #[serde(rename = "proof")]
144 #[serde(deserialize_with = "deserialize_required")]
145 pub proof: String,
146 #[serde(rename = "subject")]
147 #[serde(deserialize_with = "deserialize_required")]
148 pub subject: String,
149}
150
151impl fmt::Debug for AuthenticateResponseAssertion {
152 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153 formatter
154 .debug_struct("AuthenticateResponseAssertion")
155 .field("actor_kind", &self.actor_kind)
156 .field("assurance", &self.assurance)
157 .field("audience", &self.audience)
158 .field("claims", &self.claims)
159 .field("expires_at", &self.expires_at)
160 .field("issued_at", &self.issued_at)
161 .field("issuer", &self.issuer)
162 .field("parent_provenance", &self.parent_provenance)
163 .field("proof", &"<redacted>")
164 .field("subject", &self.subject)
165 .finish()
166 }
167}
168
169#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
170pub enum AuthenticateResponseKind {
171 #[serde(rename = "absent")]
172 Absent,
173 #[serde(rename = "authenticated")]
174 Authenticated,
175}
176
177#[derive(Clone, Debug, PartialEq)]
178pub enum AuthenticateError {
179 Expired,
180 Invalid,
181 Revoked,
182 Unsupported,
183 Unknown(UnknownDomainError),
184}
185
186#[derive(Debug)]
187pub struct Auth;
188impl RequestCapability for Auth {
189 type Request = AuthenticateRequest;
190 type Response = AuthenticateResponse;
191 type DomainError = AuthenticateError;
192 const ID: &'static str = CAPABILITY_ID;
193 const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;
194}
195
196impl serde::Serialize for AuthenticateError {
197 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198 where
199 S: serde::Serializer,
200 {
201 use serde::ser::SerializeMap;
202 match self {
203 Self::Expired => serializer.serialize_str("expired"),
204 Self::Invalid => serializer.serialize_str("invalid"),
205 Self::Revoked => serializer.serialize_str("revoked"),
206 Self::Unsupported => serializer.serialize_str("unsupported"),
207 Self::Unknown(value) => {
208 let mut map = serializer.serialize_map(Some(1 + usize::from(value.payload.is_some()) + value.extra.len()))?;
209 map.serialize_entry("code", &value.code)?;
210 if let Some(payload) = &value.payload {
211 map.serialize_entry("payload", payload)?;
212 }
213 for (key, extra) in &value.extra {
214 map.serialize_entry(key, extra)?;
215 }
216 map.end()
217 },
218 }
219 }
220}
221
222impl<'de> serde::Deserialize<'de> for AuthenticateError {
223 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224 where
225 D: serde::Deserializer<'de>,
226 {
227 let value = <serde_json::Value as serde::Deserialize>::deserialize(deserializer)?;
228 match value {
229 serde_json::Value::String(code) => match code.as_str() {
230 "expired" => Ok(Self::Expired),
231 "invalid" => Ok(Self::Invalid),
232 "revoked" => Ok(Self::Revoked),
233 "unsupported" => Ok(Self::Unsupported),
234 _ => Ok(Self::Unknown(UnknownDomainError { code, payload: None, extra: std::collections::BTreeMap::new() })),
235 },
236 serde_json::Value::Object(mut object) => {
237 let Some(code) = object.remove("code").and_then(|value| value.as_str().map(ToOwned::to_owned)) else {
238 return Err(serde::de::Error::custom("Domain Error object is missing a string code"));
239 };
240 let payload = object.remove("payload");
241 let extra = object.into_iter().collect::<std::collections::BTreeMap<_, _>>();
242 Ok(Self::Unknown(UnknownDomainError { code, payload, extra }))
243 }
244 other => Err(serde::de::Error::custom(format!("Domain Error must be a string or object, got {other}"))),
245 }
246 }
247}
248
249pub fn encode_authenticate_request(value: &AuthenticateRequest) -> Result<String, serde_json::Error> { let value = serde_json::to_value(value)?; validate_portable_json_value(&value).map_err(portable_json_error)?; serde_json::to_string(&value) }
250pub fn decode_authenticate_request(wire: &str) -> Result<AuthenticateRequest, serde_json::Error> { let value: serde_json::Value = serde_json::from_str(wire)?; validate_portable_json_value(&value).map_err(portable_json_error)?; serde_json::from_value(value) }
251pub fn encode_authenticate_response(value: &AuthenticateResponse) -> Result<String, serde_json::Error> { let value = serde_json::to_value(value)?; validate_portable_json_value(&value).map_err(portable_json_error)?; serde_json::to_string(&value) }
252pub fn decode_authenticate_response(wire: &str) -> Result<AuthenticateResponse, serde_json::Error> { let value: serde_json::Value = serde_json::from_str(wire)?; validate_portable_json_value(&value).map_err(portable_json_error)?; serde_json::from_value(value) }
253pub fn encode_authenticate_error(value: &AuthenticateError) -> Result<String, serde_json::Error> { let value = serde_json::to_value(value)?; validate_portable_json_value(&value).map_err(portable_json_error)?; serde_json::to_string(&value) }
254pub fn decode_authenticate_error(wire: &str) -> Result<AuthenticateError, serde_json::Error> { let value: serde_json::Value = serde_json::from_str(wire)?; validate_portable_json_value(&value).map_err(portable_json_error)?; serde_json::from_value(value) }
255
256pub trait AuthProvider: fmt::Debug + 'static {
257 fn authenticate(&self, context: InvocationContext, request: AuthenticateRequest) -> LocalBoxFuture<'static, Result<AuthenticateResponse, AuthInvocationError>>;
258}
259
260#[derive(Debug)]
261pub struct AuthEndpoint<P> { provider: Rc<P> }
262impl<P: AuthProvider> AuthEndpoint<P> {
263 pub fn new(provider: P) -> Self { Self { provider: Rc::new(provider) } }
264}
265
266impl<P: AuthProvider> NativeRequestEndpoint for AuthEndpoint<P> {
267 fn capability_id(&self) -> &'static str { CAPABILITY_ID }
268 fn descriptor_version(&self) -> &'static str { DESCRIPTOR_VERSION }
269 fn operations(&self) -> &'static [&'static str] { &[
270 AUTHENTICATE_OPERATION,
271 ] }
272 fn invoke(&self, operation: &str, request: Box<dyn std::any::Any>, context: InvocationContext) -> LocalBoxFuture<'static, Result<Result<Box<dyn std::any::Any>, Box<dyn std::any::Any>>, RuntimeFailure>> {
273 match operation {
274 AUTHENTICATE_OPERATION => {
275 let Ok(request) = request.downcast::<AuthenticateRequest>() else {
276 return Box::pin(futures::future::ready(Err(RuntimeFailure::ProtocolViolation { capability: CAPABILITY_ID })));
277 };
278 let provider = Rc::clone(&self.provider);
279 Box::pin(async move {
280 match provider.authenticate(context, *request).await {
281 Ok(value) => Ok(Ok(Box::new(value) as Box<dyn std::any::Any>)),
282 Err(AuthInvocationError::Domain(error)) => Ok(Err(Box::new(error) as Box<dyn std::any::Any>)),
283 Err(AuthInvocationError::Runtime(error)) => Err(error),
284 }
285 })
286 }
287 _ => Box::pin(futures::future::ready(Err(RuntimeFailure::UnknownOperation { capability: CAPABILITY_ID, operation: operation.to_owned() }))),
288 }
289 }
290}
291
292#[derive(Debug)]
293pub struct AuthClient {
294 authenticate: NativeRequestHandle<Auth>,
295}
296impl AuthClient {
297 pub fn new(handle: NativeRequestHandle<Auth>) -> Self {
298 Self { authenticate: handle }
299 }
300
301 pub fn from_dependencies(dependencies: &ModuleDependencies) -> Result<Self, RuntimeFailure> {
302 Ok(Self {
303 authenticate: dependencies.one::<Auth>()?,
304 })
305 }
306
307 pub async fn authenticate(&self, request: AuthenticateRequest) -> Result<AuthenticateResponse, AuthInvocationError> {
308 self.authenticate.invoke(AUTHENTICATE_OPERATION, request).await
309 .map_err(AuthInvocationError::Runtime)?
310 .map_err(AuthInvocationError::Domain)
311 }
312
313 pub async fn authenticate_with_context(&self, context: InvocationContext, request: AuthenticateRequest) -> Result<AuthenticateResponse, AuthInvocationError> {
314 self.authenticate.invoke_with_context(AUTHENTICATE_OPERATION, context, request).await
315 .map_err(AuthInvocationError::Runtime)?
316 .map_err(AuthInvocationError::Domain)
317 }
318}
319
320#[derive(Clone, Debug, PartialEq)]
321pub enum AuthInvocationError {
322 Domain(AuthenticateError),
323 Runtime(RuntimeFailure),
324}