Skip to main content

cloudillo_types/
identity_provider_adapter.rs

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