Skip to main content

cloudillo_types/
auth_adapter.rs

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