Skip to main content

cloudillo_types/
auth_adapter.rs

1//! Adapter that manages and stores authentication, authorization and other sensitive data.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6use std::fmt::Debug;
7
8use std::collections::HashMap;
9
10use crate::{
11	action_types,
12	prelude::*,
13	types::{serialize_timestamp_iso, serialize_timestamp_iso_opt},
14};
15
16pub const ACCESS_TOKEN_EXPIRY: i64 = 3600;
17
18/// Action tokens represent federated user actions as signed JWTs (ES384/P-384).
19///
20/// Actions are content-addressed: `action_id = "a1~" + SHA256(token)`.
21/// Field names are short (JWT claims) to minimize token size.
22#[skip_serializing_none]
23#[derive(Debug, Clone, Default, Deserialize, Serialize)]
24pub struct ActionToken {
25	/// Issuer - id_tag of the action creator (e.g., "alice.example.com")
26	pub iss: Box<str>,
27
28	/// Key ID - identifier of the signing key used (for key rotation support)
29	pub k: Box<str>,
30
31	/// Type - action type with optional subtype (e.g., "POST", "REACT:LIKE", "CONN:DEL")
32	pub t: Box<str>,
33
34	/// Content - action-specific payload as JSON.
35	pub c: Option<serde_json::Value>,
36
37	/// Parent - action_id of parent action for TRUE HIERARCHY (threading).
38	pub p: Option<Box<str>>,
39
40	/// Attachments - array of file IDs (content-addressed, e.g., "f1~abc123...")
41	pub a: Option<Vec<Box<str>>>,
42
43	/// Audience - id_tag of the target recipient.
44	pub aud: Option<Box<str>>,
45
46	/// Subject - action_id or resource_id being referenced WITHOUT creating hierarchy.
47	pub sub: Option<Box<str>>,
48
49	/// Issued At - Unix timestamp of action creation
50	pub iat: Timestamp,
51
52	/// Expires At - optional Unix timestamp for action expiration
53	pub exp: Option<Timestamp>,
54
55	/// Flags - capability flags for this action
56	pub f: Option<Box<str>>,
57
58	/// Visibility - P=Public, V=Verified, 2=2ndDegree, F=Follower, C=Connected, None=Direct
59	pub v: Option<char>,
60
61	/// Nonce - Proof-of-work nonce for rate limiting (CONN actions only).
62	#[serde(rename = "_", default, skip_serializing_if = "Option::is_none")]
63	pub nonce: Option<Box<str>>,
64}
65
66/// Access tokens are used to authenticate users
67#[skip_serializing_none]
68#[derive(Clone, Debug, Deserialize, Serialize)]
69pub struct AccessToken<S> {
70	pub iss: S,
71	pub sub: Option<S>,
72	pub scope: Option<S>,
73	pub r: Option<S>,
74	pub exp: Timestamp,
75}
76
77/// Represents a profile key
78#[skip_serializing_none]
79#[derive(Debug, Clone, Deserialize, Serialize)]
80pub struct AuthKey {
81	#[serde(rename = "keyId")]
82	pub key_id: Box<str>,
83	#[serde(rename = "publicKey")]
84	pub public_key: Box<str>,
85	#[serde(rename = "expiresAt", serialize_with = "serialize_timestamp_iso_opt")]
86	pub expires_at: Option<Timestamp>,
87}
88
89/// Represents an auth profile
90#[skip_serializing_none]
91#[derive(Debug, Deserialize, Serialize)]
92pub struct AuthProfile {
93	pub id_tag: Box<str>,
94	pub roles: Option<Box<[Box<str>]>>,
95	pub keys: Vec<AuthKey>,
96}
97
98/// Context struct for an authenticated user
99#[derive(Clone, Debug)]
100pub struct AuthCtx {
101	pub tn_id: TnId,
102	pub id_tag: Box<str>,
103	pub roles: Box<[Box<str>]>,
104	pub scope: Option<Box<str>>,
105}
106
107#[derive(Debug)]
108pub struct AuthLogin {
109	pub tn_id: TnId,
110	pub id_tag: Box<str>,
111	pub roles: Option<Box<[Box<str>]>>,
112	pub token: Box<str>,
113}
114
115/// A private/public key pair
116#[derive(Debug)]
117pub struct KeyPair {
118	pub private_key: Box<str>,
119	pub public_key: Box<str>,
120}
121
122#[derive(Debug)]
123pub struct Webauthn<'a> {
124	pub credential_id: &'a str,
125	pub counter: u32,
126	pub public_key: &'a str,
127	pub description: Option<&'a str>,
128}
129
130/// Data needed to create a new tenant
131#[derive(Debug)]
132pub struct CreateTenantData<'a> {
133	pub vfy_code: Option<&'a str>,
134	pub email: Option<&'a str>,
135	pub password: Option<&'a str>,
136	pub roles: Option<&'a [&'a str]>,
137}
138
139/// Tenant list item from auth adapter
140#[skip_serializing_none]
141#[derive(Debug, Clone, Deserialize, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct TenantListItem {
144	pub tn_id: TnId,
145	pub id_tag: Box<str>,
146	pub email: Option<Box<str>>,
147	pub roles: Option<Box<[Box<str>]>>,
148	pub status: Option<Box<str>>,
149	#[serde(serialize_with = "serialize_timestamp_iso")]
150	pub created_at: Timestamp,
151}
152
153/// Options for listing tenants
154#[derive(Debug, Default)]
155pub struct ListTenantsOptions<'a> {
156	pub status: Option<&'a str>,
157	pub q: Option<&'a str>,
158	pub limit: Option<u32>,
159	pub offset: Option<u32>,
160}
161
162/// Certificate associated with a tenant
163#[derive(Debug)]
164pub struct CertData {
165	pub tn_id: TnId,
166	pub id_tag: Box<str>,
167	pub domain: Box<str>,
168	pub cert: Box<str>,
169	pub key: Box<str>,
170	pub expires_at: Timestamp,
171}
172
173/// API key information (without the secret key)
174#[skip_serializing_none]
175#[derive(Debug, Clone, Deserialize, Serialize)]
176#[serde(rename_all = "camelCase")]
177pub struct ApiKeyInfo {
178	pub key_id: i64,
179	pub key_prefix: Box<str>,
180	pub name: Option<Box<str>>,
181	pub scopes: Option<Box<str>>,
182	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
183	pub expires_at: Option<Timestamp>,
184	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
185	pub last_used_at: Option<Timestamp>,
186	#[serde(serialize_with = "serialize_timestamp_iso")]
187	pub created_at: Timestamp,
188}
189
190/// Options for creating an API key
191#[derive(Debug)]
192pub struct CreateApiKeyOptions<'a> {
193	pub name: Option<&'a str>,
194	pub scopes: Option<&'a str>,
195	pub expires_at: Option<Timestamp>,
196}
197
198/// Result of creating an API key (includes plaintext key shown only once)
199#[derive(Debug)]
200pub struct CreatedApiKey {
201	pub info: ApiKeyInfo,
202	pub plaintext_key: Box<str>,
203}
204
205/// Result of validating an API key
206#[derive(Debug)]
207pub struct ApiKeyValidation {
208	pub tn_id: TnId,
209	pub id_tag: Box<str>,
210	pub key_id: i64,
211	pub scopes: Option<Box<str>>,
212	pub roles: Option<Box<str>>,
213}
214
215// Proxy site types
216// =================
217
218/// Configuration for a proxy site (stored as JSON in the config column)
219#[skip_serializing_none]
220#[derive(Debug, Clone, Default, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct ProxySiteConfig {
223	pub connect_timeout_secs: Option<u32>,
224	pub read_timeout_secs: Option<u32>,
225	pub preserve_host: Option<bool>,
226	pub proxy_protocol: Option<bool>,
227	pub custom_headers: Option<HashMap<String, String>>,
228	pub forward_headers: Option<bool>,
229	pub websocket: Option<bool>,
230}
231
232/// Proxy site data from the database
233#[skip_serializing_none]
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct ProxySiteData {
237	pub site_id: i64,
238	pub domain: Box<str>,
239	pub backend_url: Box<str>,
240	pub status: Box<str>,
241	#[serde(rename = "type")]
242	pub proxy_type: Box<str>,
243	#[serde(skip_serializing)]
244	pub cert: Option<Box<str>>,
245	#[serde(skip_serializing)]
246	pub cert_key: Option<Box<str>>,
247	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
248	pub cert_expires_at: Option<Timestamp>,
249	pub config: ProxySiteConfig,
250	pub created_by: Option<i64>,
251	#[serde(serialize_with = "serialize_timestamp_iso")]
252	pub created_at: Timestamp,
253	#[serde(serialize_with = "serialize_timestamp_iso")]
254	pub updated_at: Timestamp,
255}
256
257/// Data needed to create a new proxy site
258#[derive(Debug)]
259pub struct CreateProxySiteData<'a> {
260	pub domain: &'a str,
261	pub backend_url: &'a str,
262	pub proxy_type: &'a str,
263	pub config: &'a ProxySiteConfig,
264	pub created_by: Option<i64>,
265}
266
267/// Data to update an existing proxy site
268#[derive(Debug)]
269pub struct UpdateProxySiteData<'a> {
270	pub backend_url: Option<&'a str>,
271	pub status: Option<&'a str>,
272	pub proxy_type: Option<&'a str>,
273	pub config: Option<&'a ProxySiteConfig>,
274}
275
276/// A `Cloudillo` auth adapter
277///
278/// Every `AuthAdapter` implementation is required to implement this trait.
279/// An `AuthAdapter` is responsible for storing and managing all sensitive data used for
280/// authentication and authorization.
281#[async_trait]
282pub trait AuthAdapter: Debug + Send + Sync {
283	/// Validates an access token and returns the user context
284	async fn validate_access_token(&self, tn_id: TnId, token: &str) -> ClResult<AuthCtx>;
285
286	/// # Profiles
287	/// Reads the ID tag of the given tenant, referenced by its ID
288	async fn read_id_tag(&self, tn_id: TnId) -> ClResult<Box<str>>;
289
290	/// Reads the ID  the given tenant, referenced by its ID tag
291	async fn read_tn_id(&self, id_tag: &str) -> ClResult<TnId>;
292
293	/// Reads a tenant profile
294	async fn read_tenant(&self, id_tag: &str) -> ClResult<AuthProfile>;
295
296	/// Creates a tenant registration
297	async fn create_tenant_registration(&self, email: &str) -> ClResult<()>;
298
299	/// Creates a new tenant
300	async fn create_tenant(&self, id_tag: &str, data: CreateTenantData<'_>) -> ClResult<TnId>;
301
302	/// Deletes a tenant
303	async fn delete_tenant(&self, id_tag: &str) -> ClResult<()>;
304
305	/// Lists all tenants (for admin use)
306	async fn list_tenants(&self, opts: &ListTenantsOptions<'_>) -> ClResult<Vec<TenantListItem>>;
307
308	// Password management
309	async fn create_tenant_login(&self, id_tag: &str) -> ClResult<AuthLogin>;
310	async fn check_tenant_password(&self, id_tag: &str, password: &str) -> ClResult<AuthLogin>;
311	async fn update_tenant_password(&self, id_tag: &str, password: &str) -> ClResult<()>;
312
313	// IDP API key management
314	async fn update_idp_api_key(&self, id_tag: &str, api_key: &str) -> ClResult<()>;
315
316	// Certificate management
317	async fn create_cert(&self, cert_data: &CertData) -> ClResult<()>;
318	async fn read_cert_by_tn_id(&self, tn_id: TnId) -> ClResult<CertData>;
319	async fn read_cert_by_id_tag(&self, id_tag: &str) -> ClResult<CertData>;
320	async fn read_cert_by_domain(&self, domain: &str) -> ClResult<CertData>;
321	async fn list_all_certs(&self) -> ClResult<Vec<CertData>>;
322	async fn list_tenants_needing_cert_renewal(
323		&self,
324		renewal_days: u32,
325	) -> ClResult<Vec<(TnId, Box<str>)>>;
326
327	// Key management
328	async fn list_profile_keys(&self, tn_id: TnId) -> ClResult<Vec<AuthKey>>;
329	async fn read_profile_key(&self, tn_id: TnId, key_id: &str) -> ClResult<AuthKey>;
330	async fn create_profile_key(
331		&self,
332		tn_id: TnId,
333		expires_at: Option<Timestamp>,
334	) -> ClResult<AuthKey>;
335
336	async fn create_access_token(
337		&self,
338		tn_id: TnId,
339		data: &AccessToken<&str>,
340	) -> ClResult<Box<str>>;
341	async fn create_action_token(
342		&self,
343		tn_id: TnId,
344		data: action_types::CreateAction,
345	) -> ClResult<Box<str>>;
346	async fn create_proxy_token(
347		&self,
348		tn_id: TnId,
349		id_tag: &str,
350		roles: &[Box<str>],
351	) -> ClResult<Box<str>>;
352	async fn verify_access_token(&self, token: &str) -> ClResult<()>;
353
354	// Vapid keys
355	async fn read_vapid_key(&self, tn_id: TnId) -> ClResult<KeyPair>;
356	async fn read_vapid_public_key(&self, tn_id: TnId) -> ClResult<Box<str>>;
357	async fn create_vapid_key(&self, tn_id: TnId) -> ClResult<KeyPair>;
358	async fn update_vapid_key(&self, tn_id: TnId, key: &KeyPair) -> ClResult<()>;
359
360	// Variables
361	async fn read_var(&self, tn_id: TnId, var: &str) -> ClResult<Box<str>>;
362	async fn update_var(&self, tn_id: TnId, var: &str, value: &str) -> ClResult<()>;
363
364	// Webauthn
365	async fn list_webauthn_credentials(&self, tn_id: TnId) -> ClResult<Box<[Webauthn]>>;
366	async fn read_webauthn_credential(
367		&self,
368		tn_id: TnId,
369		credential_id: &str,
370	) -> ClResult<Webauthn>;
371	async fn create_webauthn_credential(&self, tn_id: TnId, data: &Webauthn) -> ClResult<()>;
372	async fn update_webauthn_credential_counter(
373		&self,
374		tn_id: TnId,
375		credential_id: &str,
376		counter: u32,
377	) -> ClResult<()>;
378	async fn delete_webauthn_credential(&self, tn_id: TnId, credential_id: &str) -> ClResult<()>;
379
380	// API Key management
381	async fn create_api_key(
382		&self,
383		tn_id: TnId,
384		opts: CreateApiKeyOptions<'_>,
385	) -> ClResult<CreatedApiKey>;
386	async fn validate_api_key(&self, key: &str) -> ClResult<ApiKeyValidation>;
387	async fn list_api_keys(&self, tn_id: TnId) -> ClResult<Vec<ApiKeyInfo>>;
388	async fn read_api_key(&self, tn_id: TnId, key_id: i64) -> ClResult<ApiKeyInfo>;
389	async fn update_api_key(
390		&self,
391		tn_id: TnId,
392		key_id: i64,
393		name: Option<&str>,
394		scopes: Option<&str>,
395		expires_at: Option<Timestamp>,
396	) -> ClResult<ApiKeyInfo>;
397	async fn delete_api_key(&self, tn_id: TnId, key_id: i64) -> ClResult<()>;
398	async fn cleanup_expired_api_keys(&self) -> ClResult<u32>;
399	async fn cleanup_expired_verification_codes(&self) -> ClResult<u32>;
400
401	// Proxy site management
402	async fn create_proxy_site(&self, data: &CreateProxySiteData<'_>) -> ClResult<ProxySiteData>;
403	async fn read_proxy_site(&self, site_id: i64) -> ClResult<ProxySiteData>;
404	async fn read_proxy_site_by_domain(&self, domain: &str) -> ClResult<ProxySiteData>;
405	async fn update_proxy_site(
406		&self,
407		site_id: i64,
408		data: &UpdateProxySiteData<'_>,
409	) -> ClResult<ProxySiteData>;
410	async fn delete_proxy_site(&self, site_id: i64) -> ClResult<()>;
411	async fn list_proxy_sites(&self) -> ClResult<Vec<ProxySiteData>>;
412	async fn update_proxy_site_cert(
413		&self,
414		site_id: i64,
415		cert: &str,
416		key: &str,
417		expires_at: Timestamp,
418	) -> ClResult<()>;
419	async fn list_proxy_sites_needing_cert_renewal(
420		&self,
421		renewal_days: u32,
422	) -> ClResult<Vec<ProxySiteData>>;
423}
424
425#[cfg(test)]
426mod tests {
427	use super::*;
428
429	#[test]
430	pub fn test_access_token() {
431		let token: AccessToken<String> = AccessToken {
432			iss: "a@a".into(),
433			sub: Some("b@b".into()),
434			scope: None,
435			r: None,
436			exp: Timestamp::now(),
437		};
438
439		assert_eq!(token.iss, "a@a");
440		assert_eq!(token.sub.as_ref().unwrap(), "b@b");
441	}
442}
443
444// vim: ts=4