Skip to main content

firebase_admin_sdk/auth/
tenant_mgt.rs

1//! Tenant management module.
2
3use crate::auth::{AuthError, FirebaseAuth};
4use crate::core::middleware::AuthMiddleware;
5use reqwest::Client;
6use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
7use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
8use serde::{Deserialize, Serialize};
9use url::Url;
10
11const IDENTITY_TOOLKIT_URL: &str = "https://identitytoolkit.googleapis.com/v2";
12
13/// Represents a tenant in a multi-tenant project.
14#[derive(Debug, Serialize, Deserialize, Default)]
15#[serde(rename_all = "camelCase")]
16pub struct Tenant {
17    /// The resource name of the tenant.
18    /// Format: "projects/{project-id}/tenants/{tenant-id}"
19    pub name: String,
20
21    /// The display name of the tenant.
22    pub display_name: Option<String>,
23
24    /// Whether to allow email/password user authentication.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub allow_password_signup: Option<bool>,
27
28    /// Whether to enable email link user authentication.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub enable_email_link_signin: Option<bool>,
31
32    /// Whether authentication is disabled for the tenant.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub disable_auth: Option<bool>,
35
36    /// Whether to enable anonymous user authentication.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub enable_anonymous_user: Option<bool>,
39
40    /// Map of test phone numbers and their fake verification codes.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
43
44    /// The tenant-level configuration of MFA options.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub mfa_config: Option<serde_json::Value>,
47
48    /// The tenant-level reCAPTCHA config.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub recaptcha_config: Option<serde_json::Value>,
51
52    /// Configures which regions are enabled for SMS verification.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub sms_region_config: Option<serde_json::Value>,
55
56    /// Configuration related to monitoring project activity.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub monitoring: Option<serde_json::Value>,
59
60    /// The tenant-level password policy config.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub password_policy_config: Option<serde_json::Value>,
63
64    /// Configuration for settings related to email privacy.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub email_privacy_config: Option<serde_json::Value>,
67
68    /// Options related to how clients making requests on behalf of a project should be configured.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub client: Option<serde_json::Value>,
71}
72
73/// Request to create a new tenant.
74#[derive(Debug, Serialize, Default)]
75#[serde(rename_all = "camelCase")]
76pub struct CreateTenantRequest {
77    /// The display name of the tenant.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub display_name: Option<String>,
80
81    /// Whether to allow email/password user authentication.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub allow_password_signup: Option<bool>,
84
85    /// Whether to enable email link user authentication.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub enable_email_link_signin: Option<bool>,
88
89    /// Whether authentication is disabled for the tenant.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub disable_auth: Option<bool>,
92
93    /// Whether to enable anonymous user authentication.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub enable_anonymous_user: Option<bool>,
96
97    /// Map of test phone numbers and their fake verification codes.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
100
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub mfa_config: Option<serde_json::Value>,
103
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub recaptcha_config: Option<serde_json::Value>,
106
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub sms_region_config: Option<serde_json::Value>,
109
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub monitoring: Option<serde_json::Value>,
112
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub password_policy_config: Option<serde_json::Value>,
115
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub email_privacy_config: Option<serde_json::Value>,
118
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub client: Option<serde_json::Value>,
121}
122
123/// Request to update a tenant.
124#[derive(Debug, Serialize, Default)]
125#[serde(rename_all = "camelCase")]
126pub struct UpdateTenantRequest {
127    /// The display name of the tenant.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub display_name: Option<String>,
130
131    /// Whether to allow email/password user authentication.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub allow_password_signup: Option<bool>,
134
135    /// Whether to enable email link user authentication.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub enable_email_link_signin: Option<bool>,
138
139    /// Whether authentication is disabled for the tenant.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub disable_auth: Option<bool>,
142
143    /// Whether to enable anonymous user authentication.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub enable_anonymous_user: Option<bool>,
146
147    /// Map of test phone numbers and their fake verification codes.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub test_phone_numbers: Option<std::collections::HashMap<String, String>>,
150
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub mfa_config: Option<serde_json::Value>,
153
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub recaptcha_config: Option<serde_json::Value>,
156
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub sms_region_config: Option<serde_json::Value>,
159
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub monitoring: Option<serde_json::Value>,
162
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub password_policy_config: Option<serde_json::Value>,
165
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub email_privacy_config: Option<serde_json::Value>,
168
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub client: Option<serde_json::Value>,
171}
172
173/// Response from listing tenants.
174#[derive(Debug, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct ListTenantsResponse {
177    /// The list of tenants.
178    pub tenants: Option<Vec<Tenant>>,
179    /// The token for the next page of results.
180    pub next_page_token: Option<String>,
181}
182
183/// Manages tenants in a multi-tenant project.
184#[derive(Clone)]
185pub struct TenantAwareness {
186    client: ClientWithMiddleware,
187    base_url: String,
188    middleware: AuthMiddleware,
189}
190
191impl TenantAwareness {
192    pub(crate) fn new(middleware: AuthMiddleware) -> Self {
193        let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
194        let client = ClientBuilder::new(Client::new())
195            .with(RetryTransientMiddleware::new_with_policy(retry_policy))
196            .with(middleware.clone())
197            .build();
198
199        let project_id = middleware.key.project_id.clone().unwrap_or_default();
200        let base_url = format!("{}/projects/{}", IDENTITY_TOOLKIT_URL, project_id);
201
202        Self {
203            client,
204            base_url,
205            middleware,
206        }
207    }
208
209    /// Returns a `FirebaseAuth` instance scoped to the specified tenant.
210    pub fn auth_for_tenant(&self, tenant_id: &str) -> FirebaseAuth {
211        let middleware = self.middleware.with_tenant(tenant_id);
212        FirebaseAuth::new(middleware)
213    }
214
215    /// Creates a new tenant.
216    pub async fn create_tenant(&self, request: CreateTenantRequest) -> Result<Tenant, AuthError> {
217        let url = format!("{}/tenants", self.base_url);
218
219        let response = self
220            .client
221            .post(&url)
222            .json(&request)
223            .send()
224            .await?;
225
226        if !response.status().is_success() {
227            let status = response.status();
228            let text = response.text().await.unwrap_or_default();
229            return Err(AuthError::ApiError(format!(
230                "Create tenant failed {}: {}",
231                status, text
232            )));
233        }
234
235        let tenant: Tenant = response.json().await?;
236        Ok(tenant)
237    }
238
239    /// Retrieves a tenant by ID.
240    pub async fn get_tenant(&self, tenant_id: &str) -> Result<Tenant, AuthError> {
241        let url = format!("{}/tenants/{}", self.base_url, tenant_id);
242
243        let response = self.client.get(&url).send().await?;
244
245        if !response.status().is_success() {
246            let status = response.status();
247            let text = response.text().await.unwrap_or_default();
248            return Err(AuthError::ApiError(format!(
249                "Get tenant failed {}: {}",
250                status, text
251            )));
252        }
253
254        let tenant: Tenant = response.json().await?;
255        Ok(tenant)
256    }
257
258    /// Updates a tenant.
259    pub async fn update_tenant(
260        &self,
261        tenant_id: &str,
262        request: UpdateTenantRequest,
263    ) -> Result<Tenant, AuthError> {
264        let url = format!("{}/tenants/{}", self.base_url, tenant_id);
265
266        let mut mask_parts = Vec::new();
267        if request.display_name.is_some() { mask_parts.push("displayName"); }
268        if request.allow_password_signup.is_some() { mask_parts.push("allowPasswordSignup"); }
269        if request.enable_email_link_signin.is_some() { mask_parts.push("enableEmailLinkSignin"); }
270        if request.disable_auth.is_some() { mask_parts.push("disableAuth"); }
271        if request.enable_anonymous_user.is_some() { mask_parts.push("enableAnonymousUser"); }
272        if request.test_phone_numbers.is_some() { mask_parts.push("testPhoneNumbers"); }
273        if request.mfa_config.is_some() { mask_parts.push("mfaConfig"); }
274        if request.recaptcha_config.is_some() { mask_parts.push("recaptchaConfig"); }
275        if request.sms_region_config.is_some() { mask_parts.push("smsRegionConfig"); }
276        if request.monitoring.is_some() { mask_parts.push("monitoring"); }
277        if request.password_policy_config.is_some() { mask_parts.push("passwordPolicyConfig"); }
278        if request.email_privacy_config.is_some() { mask_parts.push("emailPrivacyConfig"); }
279        if request.client.is_some() { mask_parts.push("client"); }
280
281        let update_mask = mask_parts.join(",");
282
283        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
284        url_obj.query_pairs_mut().append_pair("updateMask", &update_mask);
285
286        let response = self
287            .client
288            .patch(url_obj)
289            .json(&request)
290            .send()
291            .await?;
292
293        if !response.status().is_success() {
294            let status = response.status();
295            let text = response.text().await.unwrap_or_default();
296            return Err(AuthError::ApiError(format!(
297                "Update tenant failed {}: {}",
298                status, text
299            )));
300        }
301
302        let tenant: Tenant = response.json().await?;
303        Ok(tenant)
304    }
305
306    /// Deletes a tenant.
307    pub async fn delete_tenant(&self, tenant_id: &str) -> Result<(), AuthError> {
308        let url = format!("{}/tenants/{}", self.base_url, tenant_id);
309
310        let response = self.client.delete(&url).send().await?;
311
312        if !response.status().is_success() {
313            let status = response.status();
314            let text = response.text().await.unwrap_or_default();
315            return Err(AuthError::ApiError(format!(
316                "Delete tenant failed {}: {}",
317                status, text
318            )));
319        }
320
321        Ok(())
322    }
323
324    /// Lists tenants.
325    pub async fn list_tenants(
326        &self,
327        max_results: Option<u32>,
328        page_token: Option<&str>,
329    ) -> Result<ListTenantsResponse, AuthError> {
330        let url = format!("{}/tenants", self.base_url);
331        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
332
333        {
334            let mut query_pairs = url_obj.query_pairs_mut();
335            if let Some(max) = max_results {
336                query_pairs.append_pair("pageSize", &max.to_string());
337            }
338            if let Some(token) = page_token {
339                query_pairs.append_pair("pageToken", token);
340            }
341        }
342
343        let response = self.client.get(url_obj).send().await?;
344
345        if !response.status().is_success() {
346            let status = response.status();
347            let text = response.text().await.unwrap_or_default();
348            return Err(AuthError::ApiError(format!(
349                "List tenants failed {}: {}",
350                status, text
351            )));
352        }
353
354        let result: ListTenantsResponse = response.json().await?;
355        Ok(result)
356    }
357}