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#[skip_serializing_none]
94#[derive(Debug, Deserialize, Serialize)]
95pub struct AuthProfile {
96	pub id_tag: Box<str>,
97	pub roles: Option<Box<[Box<str>]>>,
98	pub keys: Vec<AuthKey>,
99}
100
101/// Context struct for an authenticated user
102#[derive(Clone, Debug)]
103pub struct AuthCtx {
104	pub tn_id: TnId,
105	pub id_tag: Box<str>,
106	pub roles: Box<[Box<str>]>,
107	pub scope: Option<Box<str>>,
108}
109
110#[derive(Debug)]
111pub struct AuthLogin {
112	pub tn_id: TnId,
113	pub id_tag: Box<str>,
114	pub roles: Option<Box<[Box<str>]>>,
115	pub token: Box<str>,
116}
117
118/// A private/public key pair
119#[derive(Debug)]
120pub struct KeyPair {
121	pub private_key: Box<str>,
122	pub public_key: Box<str>,
123}
124
125#[derive(Debug)]
126pub struct Webauthn<'a> {
127	pub credential_id: &'a str,
128	pub counter: u32,
129	pub public_key: &'a str,
130	pub description: Option<&'a str>,
131}
132
133/// Data needed to create a new tenant
134#[derive(Debug)]
135pub struct CreateTenantData<'a> {
136	pub vfy_code: Option<&'a str>,
137	pub email: Option<&'a str>,
138	pub password: Option<&'a str>,
139	pub roles: Option<&'a [&'a str]>,
140}
141
142/// Tenant list item from auth adapter
143#[skip_serializing_none]
144#[derive(Debug, Clone, Deserialize, Serialize)]
145#[serde(rename_all = "camelCase")]
146pub struct TenantListItem {
147	pub tn_id: TnId,
148	pub id_tag: Box<str>,
149	pub email: Option<Box<str>>,
150	pub roles: Option<Box<[Box<str>]>>,
151	pub status: Option<Box<str>>,
152	#[serde(serialize_with = "serialize_timestamp_iso")]
153	pub created_at: Timestamp,
154}
155
156/// Options for listing tenants
157#[derive(Debug, Default)]
158pub struct ListTenantsOptions<'a> {
159	pub status: Option<&'a str>,
160	pub q: Option<&'a str>,
161	pub limit: Option<u32>,
162	pub offset: Option<u32>,
163}
164
165/// Certificate associated with a tenant
166#[derive(Debug)]
167pub struct CertData {
168	pub tn_id: TnId,
169	pub id_tag: Box<str>,
170	pub domain: Box<str>,
171	pub cert: Box<str>,
172	pub key: Box<str>,
173	pub expires_at: Timestamp,
174}
175
176/// API key information (without the secret key)
177#[skip_serializing_none]
178#[derive(Debug, Clone, Deserialize, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct ApiKeyInfo {
181	pub key_id: i64,
182	pub key_prefix: Box<str>,
183	pub name: Option<Box<str>>,
184	pub scopes: Option<Box<str>>,
185	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
186	pub expires_at: Option<Timestamp>,
187	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
188	pub last_used_at: Option<Timestamp>,
189	#[serde(serialize_with = "serialize_timestamp_iso")]
190	pub created_at: Timestamp,
191}
192
193/// Options for creating an API key
194#[derive(Debug)]
195pub struct CreateApiKeyOptions<'a> {
196	pub name: Option<&'a str>,
197	pub scopes: Option<&'a str>,
198	pub expires_at: Option<Timestamp>,
199}
200
201/// Result of creating an API key (includes plaintext key shown only once)
202#[derive(Debug)]
203pub struct CreatedApiKey {
204	pub info: ApiKeyInfo,
205	pub plaintext_key: Box<str>,
206}
207
208/// Result of validating an API key
209#[derive(Debug)]
210pub struct ApiKeyValidation {
211	pub tn_id: TnId,
212	pub id_tag: Box<str>,
213	pub key_id: i64,
214	pub scopes: Option<Box<str>>,
215	pub roles: Option<Box<str>>,
216}
217
218// Proxy site types
219// =================
220
221/// Configuration for a proxy site (stored as JSON in the config column)
222#[skip_serializing_none]
223#[derive(Debug, Clone, Default, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct ProxySiteConfig {
226	pub connect_timeout_secs: Option<u32>,
227	pub read_timeout_secs: Option<u32>,
228	pub preserve_host: Option<bool>,
229	pub proxy_protocol: Option<bool>,
230	pub custom_headers: Option<HashMap<String, String>>,
231	pub forward_headers: Option<bool>,
232	pub websocket: Option<bool>,
233}
234
235/// Proxy site data from the database
236#[skip_serializing_none]
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct ProxySiteData {
240	pub site_id: i64,
241	pub domain: Box<str>,
242	pub backend_url: Box<str>,
243	pub status: Box<str>,
244	#[serde(rename = "type")]
245	pub proxy_type: Box<str>,
246	#[serde(skip_serializing)]
247	pub cert: Option<Box<str>>,
248	#[serde(skip_serializing)]
249	pub cert_key: Option<Box<str>>,
250	#[serde(serialize_with = "serialize_timestamp_iso_opt")]
251	pub cert_expires_at: Option<Timestamp>,
252	pub config: ProxySiteConfig,
253	pub created_by: Option<i64>,
254	#[serde(serialize_with = "serialize_timestamp_iso")]
255	pub created_at: Timestamp,
256	#[serde(serialize_with = "serialize_timestamp_iso")]
257	pub updated_at: Timestamp,
258}
259
260/// Data needed to create a new proxy site
261#[derive(Debug)]
262pub struct CreateProxySiteData<'a> {
263	pub domain: &'a str,
264	pub backend_url: &'a str,
265	pub proxy_type: &'a str,
266	pub config: &'a ProxySiteConfig,
267	pub created_by: Option<i64>,
268}
269
270/// Data to update an existing proxy site
271#[derive(Debug)]
272pub struct UpdateProxySiteData<'a> {
273	pub backend_url: Option<&'a str>,
274	pub status: Option<&'a str>,
275	pub proxy_type: Option<&'a str>,
276	pub config: Option<&'a ProxySiteConfig>,
277}
278
279/// A `Cloudillo` auth adapter
280///
281/// Every `AuthAdapter` implementation is required to implement this trait.
282/// An `AuthAdapter` is responsible for storing and managing all sensitive data used for
283/// authentication and authorization.
284#[async_trait]
285pub trait AuthAdapter: Debug + Send + Sync {
286	/// Validates an access token and returns the user context
287	async fn validate_access_token(&self, tn_id: TnId, token: &str) -> ClResult<AuthCtx>;
288
289	/// # Profiles
290	/// Reads the ID tag of the given tenant, referenced by its ID
291	async fn read_id_tag(&self, tn_id: TnId) -> ClResult<Box<str>>;
292
293	/// Reads the ID  the given tenant, referenced by its ID tag
294	async fn read_tn_id(&self, id_tag: &str) -> ClResult<TnId>;
295
296	/// Reads a tenant profile
297	async fn read_tenant(&self, id_tag: &str) -> ClResult<AuthProfile>;
298
299	/// Creates a tenant registration
300	async fn create_tenant_registration(&self, email: &str) -> ClResult<()>;
301
302	/// Creates a new tenant
303	async fn create_tenant(&self, id_tag: &str, data: CreateTenantData<'_>) -> ClResult<TnId>;
304
305	/// Deletes a tenant
306	async fn delete_tenant(&self, id_tag: &str) -> ClResult<()>;
307
308	/// Lists all tenants (for admin use)
309	async fn list_tenants(&self, opts: &ListTenantsOptions<'_>) -> ClResult<Vec<TenantListItem>>;
310
311	// Password management
312	async fn create_tenant_login(&self, id_tag: &str) -> ClResult<AuthLogin>;
313	async fn check_tenant_password(&self, id_tag: &str, password: &str) -> ClResult<AuthLogin>;
314	async fn update_tenant_password(&self, id_tag: &str, password: &str) -> ClResult<()>;
315
316	// IDP API key management
317	async fn update_idp_api_key(&self, id_tag: &str, api_key: &str) -> ClResult<()>;
318
319	// Certificate management
320	async fn create_cert(&self, cert_data: &CertData) -> ClResult<()>;
321	async fn read_cert_by_tn_id(&self, tn_id: TnId) -> ClResult<CertData>;
322	async fn read_cert_by_id_tag(&self, id_tag: &str) -> ClResult<CertData>;
323	async fn read_cert_by_domain(&self, domain: &str) -> ClResult<CertData>;
324	async fn list_all_certs(&self) -> ClResult<Vec<CertData>>;
325	async fn list_tenants_needing_cert_renewal(
326		&self,
327		renewal_days: u32,
328	) -> ClResult<Vec<(TnId, Box<str>)>>;
329
330	// Key management
331	async fn list_profile_keys(&self, tn_id: TnId) -> ClResult<Vec<AuthKey>>;
332	async fn read_profile_key(&self, tn_id: TnId, key_id: &str) -> ClResult<AuthKey>;
333	async fn create_profile_key(
334		&self,
335		tn_id: TnId,
336		expires_at: Option<Timestamp>,
337	) -> ClResult<AuthKey>;
338
339	async fn create_access_token(
340		&self,
341		tn_id: TnId,
342		data: &AccessToken<&str>,
343	) -> ClResult<Box<str>>;
344	async fn create_action_token(
345		&self,
346		tn_id: TnId,
347		data: action_types::CreateAction,
348	) -> ClResult<Box<str>>;
349	async fn verify_access_token(&self, token: &str) -> ClResult<()>;
350
351	// Vapid keys
352	async fn read_vapid_key(&self, tn_id: TnId) -> ClResult<KeyPair>;
353	async fn read_vapid_public_key(&self, tn_id: TnId) -> ClResult<Box<str>>;
354	async fn create_vapid_key(&self, tn_id: TnId) -> ClResult<KeyPair>;
355	async fn update_vapid_key(&self, tn_id: TnId, key: &KeyPair) -> ClResult<()>;
356
357	// Variables
358	async fn read_var(&self, tn_id: TnId, var: &str) -> ClResult<Box<str>>;
359	async fn update_var(&self, tn_id: TnId, var: &str, value: &str) -> ClResult<()>;
360
361	// Webauthn
362	async fn list_webauthn_credentials(&self, tn_id: TnId) -> ClResult<Box<[Webauthn]>>;
363	async fn read_webauthn_credential(
364		&self,
365		tn_id: TnId,
366		credential_id: &str,
367	) -> ClResult<Webauthn>;
368	async fn create_webauthn_credential(&self, tn_id: TnId, data: &Webauthn) -> ClResult<()>;
369	async fn update_webauthn_credential_counter(
370		&self,
371		tn_id: TnId,
372		credential_id: &str,
373		counter: u32,
374	) -> ClResult<()>;
375	async fn delete_webauthn_credential(&self, tn_id: TnId, credential_id: &str) -> ClResult<()>;
376
377	// API Key management
378	async fn create_api_key(
379		&self,
380		tn_id: TnId,
381		opts: CreateApiKeyOptions<'_>,
382	) -> ClResult<CreatedApiKey>;
383	async fn validate_api_key(&self, key: &str) -> ClResult<ApiKeyValidation>;
384	async fn list_api_keys(&self, tn_id: TnId) -> ClResult<Vec<ApiKeyInfo>>;
385	async fn read_api_key(&self, tn_id: TnId, key_id: i64) -> ClResult<ApiKeyInfo>;
386	async fn update_api_key(
387		&self,
388		tn_id: TnId,
389		key_id: i64,
390		name: Option<&str>,
391		scopes: Option<&str>,
392		expires_at: Option<Timestamp>,
393	) -> ClResult<ApiKeyInfo>;
394	async fn delete_api_key(&self, tn_id: TnId, key_id: i64) -> ClResult<()>;
395	async fn cleanup_expired_api_keys(&self) -> ClResult<u32>;
396	async fn cleanup_expired_verification_codes(&self) -> ClResult<u32>;
397
398	// Proxy site management
399	async fn create_proxy_site(&self, data: &CreateProxySiteData<'_>) -> ClResult<ProxySiteData>;
400	async fn read_proxy_site(&self, site_id: i64) -> ClResult<ProxySiteData>;
401	async fn read_proxy_site_by_domain(&self, domain: &str) -> ClResult<ProxySiteData>;
402	async fn update_proxy_site(
403		&self,
404		site_id: i64,
405		data: &UpdateProxySiteData<'_>,
406	) -> ClResult<ProxySiteData>;
407	async fn delete_proxy_site(&self, site_id: i64) -> ClResult<()>;
408	async fn list_proxy_sites(&self) -> ClResult<Vec<ProxySiteData>>;
409	async fn update_proxy_site_cert(
410		&self,
411		site_id: i64,
412		cert: &str,
413		key: &str,
414		expires_at: Timestamp,
415	) -> ClResult<()>;
416	async fn list_proxy_sites_needing_cert_renewal(
417		&self,
418		renewal_days: u32,
419	) -> ClResult<Vec<ProxySiteData>>;
420}
421
422#[cfg(test)]
423mod tests {
424	use super::*;
425
426	#[test]
427	pub fn test_access_token() {
428		let token: AccessToken<String> = AccessToken {
429			iss: "a@a".into(),
430			sub: Some("b@b".into()),
431			scope: None,
432			r: None,
433			exp: Timestamp::now(),
434		};
435
436		assert_eq!(token.iss, "a@a");
437		assert_eq!(token.sub.as_ref().unwrap(), "b@b");
438	}
439}
440
441// vim: ts=4