Skip to main content

cloudillo_types/
identity_provider_adapter.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Adapter that manages identity registration and DNS modifications.
5//!
6//! The Identity Provider Adapter is responsible for handling DNS modifications
7//! for identity registration. Each identity (id_tag) is associated with an email
8//! address and has lifecycle timestamps.
9
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13
14pub use crate::address::AddressType;
15use crate::prelude::*;
16
17/// Status of an identity in the registration lifecycle
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum IdentityStatus {
20	/// Identity is awaiting activation/validation
21	Pending,
22	/// Identity is active and can be used
23	Active,
24	/// Identity is suspended and cannot be used
25	Suspended,
26}
27
28impl std::fmt::Display for IdentityStatus {
29	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30		match self {
31			IdentityStatus::Pending => write!(f, "pending"),
32			IdentityStatus::Active => write!(f, "active"),
33			IdentityStatus::Suspended => write!(f, "suspended"),
34		}
35	}
36}
37
38impl std::str::FromStr for IdentityStatus {
39	type Err = Error;
40	fn from_str(s: &str) -> Result<Self, Self::Err> {
41		match s {
42			"pending" => Ok(IdentityStatus::Pending),
43			"active" => Ok(IdentityStatus::Active),
44			"suspended" => Ok(IdentityStatus::Suspended),
45			_ => Err(Error::ValidationError(format!("invalid identity status: {}", s))),
46		}
47	}
48}
49
50/// Quota tracking for identity registrations
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct RegistrarQuota {
53	/// The registrar's id_tag
54	pub registrar_id_tag: Box<str>,
55	/// Maximum number of identities this registrar can create
56	pub max_identities: i32,
57	/// Maximum total storage for all identities (in bytes)
58	pub max_storage_bytes: i64,
59	/// Current count of identities created by this registrar
60	pub current_identities: i32,
61	/// Current storage used by this registrar (in bytes)
62	pub current_storage_bytes: i64,
63	/// Timestamp when the quota was last updated
64	pub updated_at: Timestamp,
65}
66
67/// Represents an identity registration
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Identity {
70	/// Unique identifier prefix (local part) for this identity
71	pub id_tag_prefix: Box<str>,
72	/// Domain part of the identity (e.g., cloudillo.net)
73	pub id_tag_domain: Box<str>,
74	/// Email address associated with this identity (optional for community-owned identities)
75	pub email: Option<Box<str>>,
76	/// ID tag of the registrar who created this identity
77	pub registrar_id_tag: Box<str>,
78	/// ID tag of the owner who controls this identity (if different from registrar)
79	/// When set, the owner has permanent control; registrar only has control while Pending
80	pub owner_id_tag: Option<Box<str>>,
81	/// Address (DNS record, server address, or other routing info)
82	pub address: Option<Box<str>>,
83	/// Type of the address (IPv4, IPv6, or Hostname)
84	pub address_type: Option<AddressType>,
85	/// Timestamp when the address was last updated
86	pub address_updated_at: Option<Timestamp>,
87	/// Whether this identity uses dynamic DNS (60s TTL instead of 3600s)
88	pub dyndns: bool,
89	/// Preferred language for emails and notifications (e.g., "hu", "de")
90	pub lang: Option<Box<str>>,
91	/// Status of this identity in its lifecycle
92	pub status: IdentityStatus,
93	/// Timestamp when the identity was created
94	pub created_at: Timestamp,
95	/// Timestamp when the identity was last updated
96	pub updated_at: Timestamp,
97	/// Timestamp when the identity expires
98	pub expires_at: Timestamp,
99}
100
101/// Options for creating a new identity
102#[derive(Debug, Clone)]
103pub struct CreateIdentityOptions<'a> {
104	/// The unique identifier prefix (local part) for this identity
105	pub id_tag_prefix: &'a str,
106	/// The domain part of the identity identifier
107	pub id_tag_domain: &'a str,
108	/// Email address to associate with this identity (optional for community-owned identities)
109	pub email: Option<&'a str>,
110	/// The id_tag of the registrar creating this identity
111	pub registrar_id_tag: &'a str,
112	/// The id_tag of the owner who will control this identity (optional)
113	/// When issuer="owner" in the registration token, this is set from the token issuer
114	pub owner_id_tag: Option<&'a str>,
115	/// Initial status of the identity (default: Pending)
116	pub status: IdentityStatus,
117	/// Initial address for this identity (optional)
118	pub address: Option<&'a str>,
119	/// Type of the address being set (if address is provided)
120	pub address_type: Option<AddressType>,
121	/// Whether this identity uses dynamic DNS (60s TTL instead of 3600s)
122	pub dyndns: bool,
123	/// Preferred language for emails and notifications (e.g., "hu", "de")
124	pub lang: Option<&'a str>,
125	/// When the identity should expire (optional, can have default)
126	pub expires_at: Option<Timestamp>,
127}
128
129/// Options for updating an existing identity
130#[derive(Debug, Clone, Default)]
131pub struct UpdateIdentityOptions {
132	/// New email address (if changing)
133	pub email: Option<Box<str>>,
134	/// New owner id_tag (for ownership transfer)
135	pub owner_id_tag: Option<Box<str>>,
136	/// New address (if changing)
137	pub address: Option<Box<str>>,
138	/// Type of the address being set (if address is provided)
139	pub address_type: Option<AddressType>,
140	/// Whether to use dynamic DNS (60s TTL instead of 3600s)
141	pub dyndns: Option<bool>,
142	/// New preferred language (if changing)
143	pub lang: Option<Option<Box<str>>>,
144	/// New status (if changing)
145	pub status: Option<IdentityStatus>,
146	/// New expiration timestamp (if changing)
147	pub expires_at: Option<Timestamp>,
148}
149
150/// Options for listing identities
151#[derive(Debug, Clone)]
152pub struct ListIdentityOptions {
153	/// Filter by identity domain (the domain part of id_tag, e.g., "home.w9.hu")
154	/// This is REQUIRED - only show identities belonging to this domain
155	pub id_tag_domain: String,
156	/// Filter by email address (partial match)
157	pub email: Option<String>,
158	/// Filter by registrar id_tag
159	pub registrar_id_tag: Option<String>,
160	/// Filter by owner id_tag
161	pub owner_id_tag: Option<String>,
162	/// Filter by identity status
163	pub status: Option<IdentityStatus>,
164	/// Only include identities that expire after this timestamp
165	pub expires_after: Option<Timestamp>,
166	/// Only include expired identities
167	pub expired_only: bool,
168	/// Limit the number of results
169	pub limit: Option<u32>,
170	/// Offset for pagination
171	pub offset: Option<u32>,
172}
173
174/// Represents an API key in the system
175#[derive(Debug, Clone)]
176pub struct ApiKey {
177	pub id: i32,
178	pub id_tag_prefix: String,
179	pub id_tag_domain: String,
180	pub key_prefix: String,
181	pub name: Option<String>,
182	pub created_at: Timestamp,
183	pub last_used_at: Option<Timestamp>,
184	pub expires_at: Option<Timestamp>,
185}
186
187/// Options for creating a new API key
188#[derive(Debug)]
189pub struct CreateApiKeyOptions<'a> {
190	pub id_tag_prefix: &'a str,
191	pub id_tag_domain: &'a str,
192	pub name: Option<&'a str>,
193	pub expires_at: Option<Timestamp>,
194}
195
196/// Result of creating a new API key - includes the plaintext key (shown only once)
197#[derive(Debug)]
198pub struct CreatedApiKey {
199	pub api_key: ApiKey,
200	pub plaintext_key: String,
201}
202
203/// Options for listing API keys
204#[derive(Debug, Default)]
205pub struct ListApiKeyOptions {
206	pub id_tag_prefix: Option<String>,
207	pub id_tag_domain: Option<String>,
208	pub limit: Option<u32>,
209	pub offset: Option<u32>,
210}
211
212/// A `Cloudillo` identity provider adapter
213///
214/// Every `IdentityProviderAdapter` implementation is required to implement this trait.
215/// An `IdentityProviderAdapter` is responsible for managing identity registrations
216/// and handling DNS modifications for identity registration.
217#[async_trait]
218pub trait IdentityProviderAdapter: Debug + Send + Sync {
219	/// Creates a new identity registration
220	///
221	/// This method registers a new identity with the given id_tag and email address.
222	/// It should also handle any necessary DNS modifications for the identity.
223	///
224	/// # Arguments
225	/// * `opts` - Options containing id_tag, email, and optional expiration
226	///
227	/// # Returns
228	/// The newly created `Identity` with all timestamps populated
229	///
230	/// # Errors
231	/// Returns an error if:
232	/// - The id_tag already exists
233	/// - The email is invalid or already in use
234	/// - DNS modifications fail
235	async fn create_identity(&self, opts: CreateIdentityOptions<'_>) -> ClResult<Identity>;
236
237	/// Reads an identity by its id_tag
238	///
239	/// # Arguments
240	/// * `id_tag` - The unique identifier tag to look up
241	///
242	/// # Returns
243	/// `Some(Identity)` if found, `None` otherwise
244	async fn read_identity(
245		&self,
246		id_tag_prefix: &str,
247		id_tag_domain: &str,
248	) -> ClResult<Option<Identity>>;
249
250	/// Reads an identity by its email address
251	///
252	/// # Arguments
253	/// * `email` - The email address to look up
254	///
255	/// # Returns
256	/// `Some(Identity)` if found, `None` otherwise
257	async fn read_identity_by_email(&self, email: &str) -> ClResult<Option<Identity>>;
258
259	/// Updates an existing identity
260	///
261	/// # Arguments
262	/// * `id_tag` - The identifier of the identity to update
263	/// * `opts` - Options containing fields to update
264	///
265	/// # Errors
266	/// Returns an error if the identity doesn't exist or the update fails
267	async fn update_identity(
268		&self,
269		id_tag_prefix: &str,
270		id_tag_domain: &str,
271		opts: UpdateIdentityOptions,
272	) -> ClResult<Identity>;
273
274	/// Updates only the address of an identity (optimized for performance)
275	///
276	/// This method is optimized for updating just the address and address type,
277	/// avoiding unnecessary updates to other fields. Useful for frequent address updates.
278	///
279	/// # Arguments
280	/// * `id_tag` - The identifier of the identity to update
281	/// * `address` - The new address to set
282	/// * `address_type` - The type of the address (IPv4, IPv6, or Hostname)
283	///
284	/// # Returns
285	/// The updated `Identity` with the new address
286	///
287	/// # Errors
288	/// Returns an error if the identity doesn't exist or the update fails
289	async fn update_identity_address(
290		&self,
291		id_tag_prefix: &str,
292		id_tag_domain: &str,
293		address: &str,
294		address_type: AddressType,
295	) -> ClResult<Identity>;
296
297	/// Deletes an identity and cleans up associated DNS records
298	///
299	/// # Arguments
300	/// * `id_tag` - The identifier of the identity to delete
301	///
302	/// # Errors
303	/// Returns an error if the identity doesn't exist or DNS cleanup fails
304	async fn delete_identity(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<()>;
305
306	/// Lists identities matching the given criteria
307	///
308	/// # Arguments
309	/// * `opts` - Filtering and pagination options
310	///
311	/// # Returns
312	/// A vector of matching identities
313	async fn list_identities(&self, opts: ListIdentityOptions) -> ClResult<Vec<Identity>>;
314
315	/// Checks if an identity exists
316	///
317	/// # Arguments
318	/// * `id_tag` - The identifier to check
319	///
320	/// # Returns
321	/// `true` if the identity exists, `false` otherwise
322	async fn identity_exists(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<bool> {
323		Ok(self.read_identity(id_tag_prefix, id_tag_domain).await?.is_some())
324	}
325
326	/// Cleans up expired identities
327	///
328	/// This method should be called periodically to remove identities that have expired.
329	/// It should also clean up any associated DNS records.
330	///
331	/// # Returns
332	/// The number of identities that were cleaned up
333	async fn cleanup_expired_identities(&self) -> ClResult<u32>;
334
335	/// Renews an identity's expiration timestamp
336	///
337	/// # Arguments
338	/// * `id_tag` - The identifier of the identity to renew
339	/// * `new_expires_at` - The new expiration timestamp
340	///
341	/// # Errors
342	/// Returns an error if the identity doesn't exist
343	async fn renew_identity(
344		&self,
345		id_tag_prefix: &str,
346		id_tag_domain: &str,
347		new_expires_at: Timestamp,
348	) -> ClResult<Identity>;
349
350	/// Creates a new API key for an identity
351	///
352	/// Returns the created API key with the plaintext key (shown only once)
353	async fn create_api_key(&self, opts: CreateApiKeyOptions<'_>) -> ClResult<CreatedApiKey>;
354
355	/// Verifies an API key and returns the associated identity if valid
356	///
357	/// Returns None if the key is invalid or expired
358	/// Updates the last_used_at timestamp on successful verification
359	///
360	/// # Security Note
361	/// Implementations MUST reject identities with the prefix 'cl-o' as it is reserved
362	/// and should not be allowed to authenticate via API keys.
363	async fn verify_api_key(&self, key: &str) -> ClResult<Option<String>>;
364
365	/// Lists API keys with optional filtering
366	///
367	/// Note: Only returns metadata, not the actual keys
368	async fn list_api_keys(&self, opts: ListApiKeyOptions) -> ClResult<Vec<ApiKey>>;
369
370	/// Deletes an API key by ID
371	async fn delete_api_key(&self, id: i32) -> ClResult<()>;
372
373	/// Deletes an API key by ID, ensuring it belongs to the specified identity
374	///
375	/// Returns true if a key was deleted, false if no matching key was found
376	async fn delete_api_key_for_identity(
377		&self,
378		id: i32,
379		id_tag_prefix: &str,
380		id_tag_domain: &str,
381	) -> ClResult<bool>;
382
383	/// Cleans up expired API keys
384	///
385	/// Returns the number of keys deleted
386	async fn cleanup_expired_api_keys(&self) -> ClResult<u32>;
387
388	/// Lists identities registered by a specific registrar
389	///
390	/// # Arguments
391	/// * `registrar_id_tag` - The registrar's id_tag
392	/// * `limit` - Optional limit on results
393	/// * `offset` - Optional pagination offset
394	///
395	/// # Returns
396	/// A vector of identities created by this registrar
397	async fn list_identities_by_registrar(
398		&self,
399		registrar_id_tag: &str,
400		limit: Option<u32>,
401		offset: Option<u32>,
402	) -> ClResult<Vec<Identity>>;
403
404	/// Gets the quota for a specific registrar
405	///
406	/// # Arguments
407	/// * `registrar_id_tag` - The registrar's id_tag
408	///
409	/// # Returns
410	/// The quota information, or an error if not found
411	async fn get_quota(&self, registrar_id_tag: &str) -> ClResult<RegistrarQuota>;
412
413	/// Sets quota limits for a registrar
414	///
415	/// # Arguments
416	/// * `registrar_id_tag` - The registrar's id_tag
417	/// * `max_identities` - Maximum number of identities allowed
418	/// * `max_storage_bytes` - Maximum storage in bytes
419	///
420	/// # Errors
421	/// Returns an error if the quota doesn't exist or update fails
422	async fn set_quota_limits(
423		&self,
424		registrar_id_tag: &str,
425		max_identities: i32,
426		max_storage_bytes: i64,
427	) -> ClResult<RegistrarQuota>;
428
429	/// Checks if a registrar has quota available for a new identity
430	///
431	/// # Arguments
432	/// * `registrar_id_tag` - The registrar's id_tag
433	/// * `storage_bytes` - Storage required for the new identity
434	///
435	/// # Returns
436	/// `true` if quota is available, `false` otherwise
437	async fn check_quota(&self, registrar_id_tag: &str, storage_bytes: i64) -> ClResult<bool>;
438
439	/// Increments the quota usage for a registrar
440	///
441	/// # Arguments
442	/// * `registrar_id_tag` - The registrar's id_tag
443	/// * `storage_bytes` - Storage bytes to add
444	///
445	/// # Errors
446	/// Returns an error if the quota doesn't exist or update fails
447	async fn increment_quota(
448		&self,
449		registrar_id_tag: &str,
450		storage_bytes: i64,
451	) -> ClResult<RegistrarQuota>;
452
453	/// Decrements the quota usage for a registrar
454	///
455	/// # Arguments
456	/// * `registrar_id_tag` - The registrar's id_tag
457	/// * `storage_bytes` - Storage bytes to subtract
458	///
459	/// # Errors
460	/// Returns an error if the quota doesn't exist or update fails
461	async fn decrement_quota(
462		&self,
463		registrar_id_tag: &str,
464		storage_bytes: i64,
465	) -> ClResult<RegistrarQuota>;
466
467	/// Updates quota counts when an identity changes status
468	///
469	/// Used when an identity is activated, suspended, or deleted to adjust quota tracking.
470	///
471	/// # Arguments
472	/// * `registrar_id_tag` - The registrar's id_tag
473	/// * `old_status` - The identity's previous status
474	/// * `new_status` - The identity's new status
475	///
476	/// # Errors
477	/// Returns an error if the quota doesn't exist or update fails
478	async fn update_quota_on_status_change(
479		&self,
480		registrar_id_tag: &str,
481		old_status: IdentityStatus,
482		new_status: IdentityStatus,
483	) -> ClResult<RegistrarQuota>;
484}
485
486#[cfg(test)]
487mod tests {
488	use super::*;
489
490	#[test]
491	fn test_identity_structure() {
492		let now = Timestamp::now();
493		let identity = Identity {
494			id_tag_prefix: "test_user".into(),
495			id_tag_domain: "cloudillo.net".into(),
496			email: Some("test@example.com".into()),
497			registrar_id_tag: "registrar".into(),
498			owner_id_tag: None,
499			address: Some("192.168.1.1".into()),
500			address_type: Some(AddressType::Ipv4),
501			address_updated_at: Some(now),
502			dyndns: false,
503			lang: Some("hu".into()),
504			status: IdentityStatus::Active,
505			created_at: now,
506			updated_at: now,
507			expires_at: now.add_seconds(86400), // 1 day later
508		};
509
510		assert_eq!(identity.id_tag_prefix.as_ref(), "test_user");
511		assert_eq!(identity.id_tag_domain.as_ref(), "cloudillo.net");
512		assert_eq!(identity.email.as_deref(), Some("test@example.com"));
513		assert_eq!(identity.registrar_id_tag.as_ref(), "registrar");
514		assert_eq!(identity.lang.as_deref(), Some("hu"));
515		assert_eq!(identity.status, IdentityStatus::Active);
516		assert!(!identity.dyndns);
517		assert!(identity.expires_at > identity.created_at);
518	}
519
520	#[test]
521	fn test_identity_with_owner() {
522		let now = Timestamp::now();
523		let identity = Identity {
524			id_tag_prefix: "community_member".into(),
525			id_tag_domain: "cloudillo.net".into(),
526			email: None, // No email for community-owned identity
527			registrar_id_tag: "registrar".into(),
528			owner_id_tag: Some("community.cloudillo.net".into()),
529			address: None,
530			address_type: None,
531			address_updated_at: None,
532			dyndns: false,
533			lang: None,
534			status: IdentityStatus::Pending,
535			created_at: now,
536			updated_at: now,
537			expires_at: now.add_seconds(86400),
538		};
539
540		assert_eq!(identity.id_tag_prefix.as_ref(), "community_member");
541		assert!(identity.email.is_none());
542		assert_eq!(identity.owner_id_tag.as_deref(), Some("community.cloudillo.net"));
543		assert_eq!(identity.status, IdentityStatus::Pending);
544	}
545
546	#[test]
547	fn test_identity_status_display() {
548		assert_eq!(IdentityStatus::Pending.to_string(), "pending");
549		assert_eq!(IdentityStatus::Active.to_string(), "active");
550		assert_eq!(IdentityStatus::Suspended.to_string(), "suspended");
551	}
552
553	#[test]
554	fn test_identity_status_from_str() {
555		use std::str::FromStr;
556		assert_eq!(
557			IdentityStatus::from_str("pending").expect("should parse"),
558			IdentityStatus::Pending
559		);
560		assert_eq!(
561			IdentityStatus::from_str("active").expect("should parse"),
562			IdentityStatus::Active
563		);
564		assert_eq!(
565			IdentityStatus::from_str("suspended").expect("should parse"),
566			IdentityStatus::Suspended
567		);
568		assert!(IdentityStatus::from_str("invalid").is_err());
569	}
570
571	#[test]
572	fn test_create_identity_options() {
573		let opts = CreateIdentityOptions {
574			id_tag_prefix: "test_user",
575			id_tag_domain: "cloudillo.net",
576			email: Some("test@example.com"),
577			registrar_id_tag: "registrar",
578			owner_id_tag: None,
579			status: IdentityStatus::Pending,
580			address: Some("192.168.1.1"),
581			address_type: Some(AddressType::Ipv4),
582			dyndns: false,
583			lang: Some("de"),
584			expires_at: Some(Timestamp::now().add_seconds(86400)),
585		};
586
587		assert_eq!(opts.id_tag_prefix, "test_user");
588		assert_eq!(opts.id_tag_domain, "cloudillo.net");
589		assert_eq!(opts.email, Some("test@example.com"));
590		assert_eq!(opts.registrar_id_tag, "registrar");
591		assert_eq!(opts.lang, Some("de"));
592		assert_eq!(opts.status, IdentityStatus::Pending);
593		assert!(!opts.dyndns);
594		assert!(opts.expires_at.is_some());
595	}
596
597	#[test]
598	fn test_create_identity_options_with_owner() {
599		let opts = CreateIdentityOptions {
600			id_tag_prefix: "member",
601			id_tag_domain: "cloudillo.net",
602			email: None, // No email for owner-managed identity
603			registrar_id_tag: "registrar",
604			owner_id_tag: Some("owner.cloudillo.net"),
605			status: IdentityStatus::Pending,
606			address: None,
607			address_type: None,
608			dyndns: false,
609			lang: None,
610			expires_at: None,
611		};
612
613		assert_eq!(opts.id_tag_prefix, "member");
614		assert!(opts.email.is_none());
615		assert_eq!(opts.owner_id_tag, Some("owner.cloudillo.net"));
616	}
617
618	#[test]
619	fn test_registrar_quota() {
620		let now = Timestamp::now();
621		let quota = RegistrarQuota {
622			registrar_id_tag: "registrar".into(),
623			max_identities: 1000,
624			max_storage_bytes: 1_000_000_000,
625			current_identities: 50,
626			current_storage_bytes: 50_000_000,
627			updated_at: now,
628		};
629
630		assert_eq!(quota.registrar_id_tag.as_ref(), "registrar");
631		assert_eq!(quota.max_identities, 1000);
632		assert!(quota.current_identities < quota.max_identities);
633	}
634}
635
636// vim: ts=4