1use cloudillo_types::address::parse_address_type;
7use cloudillo_types::identity_provider_adapter::IdentityStatus;
8use cloudillo_types::utils::parse_and_validate_identity_id_tag;
9
10use crate::prelude::*;
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct IdpRegContent {
16 pub id_tag: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub email: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub owner_id_tag: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
26 pub issuer: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub address: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub lang: Option<String>,
33}
34
35#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct IdpRegResponse {
39 pub success: bool,
40 pub message: String,
41 pub identity_status: String,
42 pub activation_ref: Option<String>,
43 pub api_key: Option<String>,
44}
45
46pub struct ProcessRegistrationParams<'a> {
48 pub reg_content: IdpRegContent,
49 pub issuer: &'a str,
50 pub audience: &'a str,
51 pub tenant_id: i64,
52 pub client_address: Option<&'a str>,
53}
54
55pub struct RegistrationResult {
57 pub identity_id: String,
58 pub activation_ref: String,
59 pub api_key_prefix: String,
60 pub plaintext_key: String,
61 pub response: IdpRegResponse,
62}
63
64pub struct SendActivationEmailParams<'a> {
66 pub id_tag_prefix: &'a str,
67 pub id_tag_domain: &'a str,
68 pub email: &'a str,
69 pub lang: Option<String>,
70}
71
72pub async fn send_activation_email(
77 app: &App,
78 tn_id: TnId,
79 params: SendActivationEmailParams<'_>,
80) -> ClResult<String> {
81 let identity_id = format!("{}.{}", params.id_tag_prefix, params.id_tag_domain);
82 let idp_domain = params.id_tag_domain;
83
84 let expires_at_ref = Some(Timestamp::now().add_seconds(24 * 60 * 60));
86 let (activation_ref, activation_link) = cloudillo_ref::service::create_ref_internal(
87 app,
88 tn_id,
89 cloudillo_ref::service::CreateRefInternalParams {
90 id_tag: idp_domain,
91 typ: "idp.activation",
92 description: Some("Identity provider activation"),
93 expires_at: expires_at_ref,
94 path_prefix: "/idp/activate",
95 resource_id: Some(&identity_id),
96 count: None,
97 },
98 )
99 .await?;
100
101 let template_vars = serde_json::json!({
103 "identity_tag": identity_id,
104 "activation_link": activation_link,
105 "identity_provider": idp_domain,
106 "expire_hours": 24,
107 });
108
109 let email_task_key = format!("email:idp-activation:{}:{}", tn_id.0, identity_id);
110 match cloudillo_email::EmailModule::schedule_email_task_with_key(
111 &app.scheduler,
112 &app.settings,
113 tn_id,
114 cloudillo_email::EmailTaskParams {
115 to: params.email.to_string(),
116 subject: None,
117 template_name: "idp-activation".to_string(),
118 template_vars,
119 lang: params.lang,
120 custom_key: Some(email_task_key),
121 from_name_override: Some(format!("{} Identity Provider", idp_domain.to_uppercase())),
122 },
123 )
124 .await
125 {
126 Ok(_) => {
127 info!(
128 id_tag_prefix = %params.id_tag_prefix,
129 id_tag_domain = %params.id_tag_domain,
130 email = %params.email,
131 "Activation email scheduled successfully"
132 );
133 }
134 Err(e) => {
135 warn!(
136 id_tag_prefix = %params.id_tag_prefix,
137 id_tag_domain = %params.id_tag_domain,
138 email = %params.email,
139 error = %e,
140 "Failed to schedule activation email"
141 );
142 }
143 }
144
145 Ok(activation_ref)
146}
147
148pub async fn process_registration(
160 app: &App,
161 params: ProcessRegistrationParams<'_>,
162) -> ClResult<RegistrationResult> {
163 let reg_content = params.reg_content;
164 let registrar_id_tag = params.issuer;
165 let target_idp = params.audience;
166
167 info!(
168 id_tag = %reg_content.id_tag,
169 issuer = %registrar_id_tag,
170 audience = %target_idp,
171 "Processing IDP registration request"
172 );
173
174 if reg_content.id_tag.is_empty() {
176 warn!(
177 id_tag = %reg_content.id_tag,
178 "IDP:REG content has empty id_tag"
179 );
180 return Err(Error::ValidationError("IDP:REG content missing id_tag".into()));
181 }
182
183 let issuer_role = reg_content.issuer.as_deref().unwrap_or("registrar");
185
186 if issuer_role != "registrar" && issuer_role != "owner" {
188 warn!(
189 issuer_role = %issuer_role,
190 "IDP:REG content has invalid issuer role"
191 );
192 return Err(Error::ValidationError(format!(
193 "Invalid issuer role '{}': must be 'registrar' or 'owner'",
194 issuer_role
195 )));
196 }
197
198 let owner_id_tag: Option<&str> = match issuer_role {
202 "owner" => {
203 Some(registrar_id_tag)
205 }
206 "registrar" => {
207 reg_content.owner_id_tag.as_deref()
209 }
210 _ => None, };
212
213 if owner_id_tag.is_none() && reg_content.email.as_ref().is_none_or(|e| e.is_empty()) {
215 warn!(
216 id_tag = %reg_content.id_tag,
217 "IDP:REG content missing email (required when no owner specified)"
218 );
219 return Err(Error::ValidationError(
220 "IDP:REG content missing email (required when no owner_id_tag is provided)".into(),
221 ));
222 }
223
224 info!(
225 id_tag = %reg_content.id_tag,
226 issuer_role = %issuer_role,
227 owner_id_tag = ?owner_id_tag,
228 email = ?reg_content.email,
229 "IDP:REG - Parsed ownership model"
230 );
231
232 let idp_adapter = app.idp_adapter.as_ref().ok_or_else(|| {
234 warn!("IDP:REG hook triggered but Identity Provider adapter not available");
235 Error::ServiceUnavailable("Identity Provider not available on this instance".to_string())
236 })?;
237
238 let (id_tag_prefix, id_tag_domain) =
242 parse_and_validate_identity_id_tag(®_content.id_tag, target_idp).map_err(|e| {
243 warn!(
244 error = %e,
245 id_tag = %reg_content.id_tag,
246 target_idp = %target_idp,
247 registrar = %registrar_id_tag,
248 "Failed to parse/validate identity id_tag against target IdP domain"
249 );
250 e
251 })?;
252
253 let address = match ®_content.address {
255 Some(addr) if addr == "auto" => {
256 params.client_address
258 }
259 Some(addr) => Some(addr.as_str()),
260 None => None,
261 };
262
263 info!(
264 id_tag = %reg_content.id_tag,
265 address = ?address,
266 client_address = ?params.client_address,
267 "Resolved address for identity (auto = client IP)"
268 );
269
270 let address_type = if let Some(addr_str) = address {
272 match parse_address_type(addr_str) {
273 Ok(addr_type) => {
274 info!(
275 address = %addr_str,
276 address_type = ?addr_type,
277 "IDP:REG - Parsed address type from resolved address"
278 );
279 Some(addr_type)
280 }
281 Err(e) => {
282 warn!(
283 address = %addr_str,
284 error = ?e,
285 "IDP:REG - Failed to parse address type"
286 );
287 None
288 }
289 }
290 } else {
291 None
292 };
293
294 let quota = idp_adapter.get_quota(registrar_id_tag).await.ok();
296 if let Some(quota) = quota {
297 if quota.current_identities >= quota.max_identities {
298 warn!(
299 registrar = %registrar_id_tag,
300 current = quota.current_identities,
301 max = quota.max_identities,
302 "Registrar quota exceeded"
303 );
304
305 let response = IdpRegResponse {
306 success: false,
307 message: "Registrar quota exceeded".to_string(),
308 identity_status: "quota_exceeded".to_string(),
309 activation_ref: None,
310 api_key: None,
311 };
312
313 return Ok(RegistrationResult {
314 identity_id: String::new(),
315 activation_ref: String::new(),
316 api_key_prefix: String::new(),
317 plaintext_key: String::new(),
318 response,
319 });
320 }
321 }
322
323 let expires_at = Timestamp::now().add_seconds(24 * 60 * 60);
325 let create_opts = cloudillo_types::identity_provider_adapter::CreateIdentityOptions {
326 id_tag_prefix: &id_tag_prefix,
327 id_tag_domain: &id_tag_domain,
328 email: reg_content.email.as_deref(),
329 registrar_id_tag,
330 owner_id_tag,
331 status: IdentityStatus::Pending,
332 address,
333 address_type,
334 dyndns: false,
335 lang: reg_content.lang.as_deref(),
336 expires_at: Some(expires_at),
337 };
338
339 info!(
340 id_tag_prefix = %id_tag_prefix,
341 id_tag_domain = %id_tag_domain,
342 address = ?address,
343 "IDP:REG - Calling IDP adapter create_identity"
344 );
345
346 let identity = idp_adapter.create_identity(create_opts).await.map_err(|e| {
347 warn!("Failed to create identity: {}", e);
348 e
349 })?;
350
351 info!(
352 id_tag_prefix = %identity.id_tag_prefix,
353 id_tag_domain = %identity.id_tag_domain,
354 registrar = %registrar_id_tag,
355 owner = ?identity.owner_id_tag,
356 email = ?identity.email,
357 address = ?identity.address,
358 "IDP:REG - Identity created with Pending status"
359 );
360
361 let create_key_opts = cloudillo_types::identity_provider_adapter::CreateApiKeyOptions {
363 id_tag_prefix: &id_tag_prefix,
364 id_tag_domain: &id_tag_domain,
365 name: Some("activation-key"),
366 expires_at: Some(Timestamp::now().add_seconds(86400)), };
368
369 let created_key = idp_adapter.create_api_key(create_key_opts).await.map_err(|e| {
370 warn!("Failed to create API key for identity: {}", e);
371 e
372 })?;
373
374 info!(
375 id_tag_prefix = %identity.id_tag_prefix,
376 id_tag_domain = %identity.id_tag_domain,
377 key_prefix = %created_key.api_key.key_prefix,
378 "API key created for identity activation"
379 );
380
381 let tn_id = TnId(params.tenant_id as u32);
383 let identity_id = format!("{}.{}", identity.id_tag_prefix, identity.id_tag_domain);
384 let activation_ref = if let Some(ref email) = identity.email {
385 match send_activation_email(
386 app,
387 tn_id,
388 SendActivationEmailParams {
389 id_tag_prefix: &identity.id_tag_prefix,
390 id_tag_domain: &identity.id_tag_domain,
391 email,
392 lang: reg_content.lang.clone(),
393 },
394 )
395 .await
396 {
397 Ok(ref_id) => ref_id,
398 Err(e) => {
399 warn!(
400 id_tag_prefix = %identity.id_tag_prefix,
401 id_tag_domain = %identity.id_tag_domain,
402 error = %e,
403 "Failed to send activation email, continuing registration"
404 );
405 String::new()
406 }
407 }
408 } else {
409 info!(
411 id_tag_prefix = %identity.id_tag_prefix,
412 id_tag_domain = %identity.id_tag_domain,
413 owner = ?identity.owner_id_tag,
414 "Identity created without email - activation via owner required"
415 );
416 String::new()
417 };
418
419 if idp_adapter.get_quota(registrar_id_tag).await.is_ok() {
421 let _ = idp_adapter.increment_quota(registrar_id_tag, 0).await; }
423
424 let message = if let Some(ref email) = identity.email {
426 format!(
427 "Identity '{}' created successfully. Activation email sent to {}",
428 reg_content.id_tag, email
429 )
430 } else {
431 format!(
432 "Identity '{}' created successfully. Activation via owner required.",
433 reg_content.id_tag
434 )
435 };
436 let response = IdpRegResponse {
437 success: true,
438 message,
439 identity_status: identity.status.to_string(),
440 activation_ref: Some(activation_ref.clone()),
441 api_key: Some(created_key.plaintext_key.clone()), };
443
444 info!(
445 id_tag_prefix = %identity.id_tag_prefix,
446 id_tag_domain = %identity.id_tag_domain,
447 registrar = %registrar_id_tag,
448 "IDP:REG registration successful"
449 );
450
451 Ok(RegistrationResult {
452 identity_id,
453 activation_ref,
454 api_key_prefix: created_key.api_key.key_prefix.to_string(),
455 plaintext_key: created_key.plaintext_key,
456 response,
457 })
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn test_idp_reg_content_parse_with_email() {
466 let json = serde_json::json!({
467 "idTag": "alice",
468 "email": "alice@example.com"
469 });
470
471 let content: IdpRegContent = serde_json::from_value(json).unwrap();
472 assert_eq!(content.id_tag, "alice");
473 assert_eq!(content.email.as_deref(), Some("alice@example.com"));
474 assert!(content.owner_id_tag.is_none());
475 assert!(content.issuer.is_none());
476 assert!(content.lang.is_none());
477 }
478
479 #[test]
480 fn test_idp_reg_content_parse_with_lang() {
481 let json = serde_json::json!({
482 "idTag": "alice",
483 "email": "alice@example.com",
484 "lang": "hu"
485 });
486
487 let content: IdpRegContent = serde_json::from_value(json).unwrap();
488 assert_eq!(content.id_tag, "alice");
489 assert_eq!(content.email.as_deref(), Some("alice@example.com"));
490 assert_eq!(content.lang.as_deref(), Some("hu"));
491 }
492
493 #[test]
494 fn test_idp_reg_content_parse_with_owner() {
495 let json = serde_json::json!({
496 "idTag": "member",
497 "ownerIdTag": "community.cloudillo.net",
498 "issuer": "registrar"
499 });
500
501 let content: IdpRegContent = serde_json::from_value(json).unwrap();
502 assert_eq!(content.id_tag, "member");
503 assert!(content.email.is_none());
504 assert_eq!(content.owner_id_tag.as_deref(), Some("community.cloudillo.net"));
505 assert_eq!(content.issuer.as_deref(), Some("registrar"));
506 }
507
508 #[test]
509 fn test_idp_reg_content_parse_issuer_owner() {
510 let json = serde_json::json!({
511 "idTag": "member",
512 "issuer": "owner"
513 });
514
515 let content: IdpRegContent = serde_json::from_value(json).unwrap();
516 assert_eq!(content.id_tag, "member");
517 assert!(content.email.is_none());
518 assert!(content.owner_id_tag.is_none()); assert_eq!(content.issuer.as_deref(), Some("owner"));
520 }
521
522 #[test]
523 fn test_idp_reg_response_serialize() {
524 let response = IdpRegResponse {
525 success: true,
526 message: "Test message".to_string(),
527 identity_status: "pending".to_string(),
528 activation_ref: Some("ref123".to_string()),
529 api_key: Some("key123".to_string()),
530 };
531
532 let json = serde_json::to_value(&response).unwrap();
533 assert!(json["success"].as_bool().unwrap());
534 assert_eq!(json["message"].as_str().unwrap(), "Test message");
535 }
536}
537
538