Skip to main content

firebase_admin_sdk/auth/
project_config_impl.rs

1//! Project configuration management (OIDC, SAML).
2
3use crate::auth::project_config::{
4    CreateOidcProviderConfigRequest, CreateSamlProviderConfigRequest,
5    ListOidcProviderConfigsResponse, ListSamlProviderConfigsResponse, OidcProviderConfig,
6    SamlProviderConfig, UpdateOidcProviderConfigRequest, UpdateSamlProviderConfigRequest,
7};
8use crate::auth::AuthError;
9use crate::core::middleware::AuthMiddleware;
10use reqwest::Client;
11use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
12use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
13use url::Url;
14
15const IDENTITY_TOOLKIT_URL: &str = "https://identitytoolkit.googleapis.com/v2";
16
17/// Manages project-level configurations like OIDC and SAML providers.
18#[derive(Clone)]
19pub struct ProjectConfig {
20    client: ClientWithMiddleware,
21    base_url: String,
22}
23
24impl ProjectConfig {
25    pub(crate) fn new(middleware: AuthMiddleware) -> Self {
26        let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
27        let client = ClientBuilder::new(Client::new())
28            .with(RetryTransientMiddleware::new_with_policy(retry_policy))
29            .with(middleware.clone())
30            .build();
31
32        let project_id = middleware.key.project_id.clone().unwrap_or_default();
33        let base_url = format!("{}/projects/{}", IDENTITY_TOOLKIT_URL, project_id);
34
35        Self { client, base_url }
36    }
37
38    #[cfg(test)]
39    pub(crate) fn new_with_client(client: ClientWithMiddleware, base_url: String) -> Self {
40        Self { client, base_url }
41    }
42
43    // --- OIDC Provider Configs ---
44
45    pub async fn create_oidc_provider_config(
46        &self,
47        request: CreateOidcProviderConfigRequest,
48    ) -> Result<OidcProviderConfig, AuthError> {
49        let url = format!("{}/oauthIdpConfigs", self.base_url);
50        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
51        url_obj.query_pairs_mut().append_pair("oauthIdpConfigId", &request.oauth_idp_config_id);
52
53        let response = self
54            .client
55            .post(url_obj)
56            .json(&request)
57            .send()
58            .await?;
59
60        if !response.status().is_success() {
61            let status = response.status();
62            let text = response.text().await.unwrap_or_default();
63            return Err(AuthError::ApiError(format!(
64                "Create OIDC config failed {}: {}",
65                status, text
66            )));
67        }
68
69        let config: OidcProviderConfig = response.json().await?;
70        Ok(config)
71    }
72
73    pub async fn get_oidc_provider_config(
74        &self,
75        config_id: &str,
76    ) -> Result<OidcProviderConfig, AuthError> {
77        let url = format!("{}/oauthIdpConfigs/{}", self.base_url, config_id);
78
79        let response = self.client.get(&url).send().await?;
80
81        if !response.status().is_success() {
82            let status = response.status();
83            let text = response.text().await.unwrap_or_default();
84            return Err(AuthError::ApiError(format!(
85                "Get OIDC config failed {}: {}",
86                status, text
87            )));
88        }
89
90        let config: OidcProviderConfig = response.json().await?;
91        Ok(config)
92    }
93
94    pub async fn update_oidc_provider_config(
95        &self,
96        config_id: &str,
97        request: UpdateOidcProviderConfigRequest,
98    ) -> Result<OidcProviderConfig, AuthError> {
99        let url = format!("{}/oauthIdpConfigs/{}", self.base_url, config_id);
100
101        let mut mask_parts = Vec::new();
102        if request.display_name.is_some() { mask_parts.push("displayName"); }
103        if request.enabled.is_some() { mask_parts.push("enabled"); }
104        if request.client_id.is_some() { mask_parts.push("clientId"); }
105        if request.issuer.is_some() { mask_parts.push("issuer"); }
106        if request.client_secret.is_some() { mask_parts.push("clientSecret"); }
107        if request.response_type.is_some() { mask_parts.push("responseType"); }
108
109        let update_mask = mask_parts.join(",");
110
111        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
112        url_obj.query_pairs_mut().append_pair("updateMask", &update_mask);
113
114        let response = self
115            .client
116            .patch(url_obj)
117            .json(&request)
118            .send()
119            .await?;
120
121        if !response.status().is_success() {
122            let status = response.status();
123            let text = response.text().await.unwrap_or_default();
124            return Err(AuthError::ApiError(format!(
125                "Update OIDC config failed {}: {}",
126                status, text
127            )));
128        }
129
130        let config: OidcProviderConfig = response.json().await?;
131        Ok(config)
132    }
133
134    pub async fn delete_oidc_provider_config(&self, config_id: &str) -> Result<(), AuthError> {
135        let url = format!("{}/oauthIdpConfigs/{}", self.base_url, config_id);
136
137        let response = self.client.delete(&url).send().await?;
138
139        if !response.status().is_success() {
140            let status = response.status();
141            let text = response.text().await.unwrap_or_default();
142            return Err(AuthError::ApiError(format!(
143                "Delete OIDC config failed {}: {}",
144                status, text
145            )));
146        }
147
148        Ok(())
149    }
150
151    pub async fn list_oidc_provider_configs(
152        &self,
153        max_results: Option<u32>,
154        page_token: Option<&str>,
155    ) -> Result<ListOidcProviderConfigsResponse, AuthError> {
156        let url = format!("{}/oauthIdpConfigs", self.base_url);
157        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
158
159        {
160            let mut query_pairs = url_obj.query_pairs_mut();
161            if let Some(max) = max_results {
162                query_pairs.append_pair("pageSize", &max.to_string());
163            }
164            if let Some(token) = page_token {
165                query_pairs.append_pair("pageToken", token);
166            }
167        }
168
169        let response = self.client.get(url_obj).send().await?;
170
171        if !response.status().is_success() {
172            let status = response.status();
173            let text = response.text().await.unwrap_or_default();
174            return Err(AuthError::ApiError(format!(
175                "List OIDC configs failed {}: {}",
176                status, text
177            )));
178        }
179
180        let result: ListOidcProviderConfigsResponse = response.json().await?;
181        Ok(result)
182    }
183
184    // --- SAML Provider Configs ---
185
186    pub async fn create_saml_provider_config(
187        &self,
188        request: CreateSamlProviderConfigRequest,
189    ) -> Result<SamlProviderConfig, AuthError> {
190        let url = format!("{}/inboundSamlConfigs", self.base_url);
191        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
192        url_obj.query_pairs_mut().append_pair("inboundSamlConfigId", &request.inbound_saml_config_id);
193
194        let response = self
195            .client
196            .post(url_obj)
197            .json(&request)
198            .send()
199            .await?;
200
201        if !response.status().is_success() {
202            let status = response.status();
203            let text = response.text().await.unwrap_or_default();
204            return Err(AuthError::ApiError(format!(
205                "Create SAML config failed {}: {}",
206                status, text
207            )));
208        }
209
210        let config: SamlProviderConfig = response.json().await?;
211        Ok(config)
212    }
213
214    pub async fn get_saml_provider_config(
215        &self,
216        config_id: &str,
217    ) -> Result<SamlProviderConfig, AuthError> {
218        let url = format!("{}/inboundSamlConfigs/{}", self.base_url, config_id);
219
220        let response = self.client.get(&url).send().await?;
221
222        if !response.status().is_success() {
223            let status = response.status();
224            let text = response.text().await.unwrap_or_default();
225            return Err(AuthError::ApiError(format!(
226                "Get SAML config failed {}: {}",
227                status, text
228            )));
229        }
230
231        let config: SamlProviderConfig = response.json().await?;
232        Ok(config)
233    }
234
235    pub async fn update_saml_provider_config(
236        &self,
237        config_id: &str,
238        request: UpdateSamlProviderConfigRequest,
239    ) -> Result<SamlProviderConfig, AuthError> {
240        let url = format!("{}/inboundSamlConfigs/{}", self.base_url, config_id);
241
242        let mut mask_parts = Vec::new();
243        if request.display_name.is_some() { mask_parts.push("displayName"); }
244        if request.enabled.is_some() { mask_parts.push("enabled"); }
245
246        // Nested fields need to be handled carefully for mask
247        if let Some(idp) = &request.idp_config {
248            if idp.idp_entity_id.is_some() { mask_parts.push("idpConfig.idpEntityId"); }
249            if idp.sso_url.is_some() { mask_parts.push("idpConfig.ssoUrl"); }
250            if idp.sign_request.is_some() { mask_parts.push("idpConfig.signRequest"); }
251            if idp.idp_certificates.is_some() { mask_parts.push("idpConfig.idpCertificates"); }
252        }
253
254        if let Some(sp) = &request.sp_config {
255            if sp.sp_entity_id.is_some() { mask_parts.push("spConfig.spEntityId"); }
256            if sp.callback_uri.is_some() { mask_parts.push("spConfig.callbackUri"); }
257        }
258
259        let update_mask = mask_parts.join(",");
260
261        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
262        url_obj.query_pairs_mut().append_pair("updateMask", &update_mask);
263
264        let response = self
265            .client
266            .patch(url_obj)
267            .json(&request)
268            .send()
269            .await?;
270
271        if !response.status().is_success() {
272            let status = response.status();
273            let text = response.text().await.unwrap_or_default();
274            return Err(AuthError::ApiError(format!(
275                "Update SAML config failed {}: {}",
276                status, text
277            )));
278        }
279
280        let config: SamlProviderConfig = response.json().await?;
281        Ok(config)
282    }
283
284    pub async fn delete_saml_provider_config(&self, config_id: &str) -> Result<(), AuthError> {
285        let url = format!("{}/inboundSamlConfigs/{}", self.base_url, config_id);
286
287        let response = self.client.delete(&url).send().await?;
288
289        if !response.status().is_success() {
290            let status = response.status();
291            let text = response.text().await.unwrap_or_default();
292            return Err(AuthError::ApiError(format!(
293                "Delete SAML config failed {}: {}",
294                status, text
295            )));
296        }
297
298        Ok(())
299    }
300
301    pub async fn list_saml_provider_configs(
302        &self,
303        max_results: Option<u32>,
304        page_token: Option<&str>,
305    ) -> Result<ListSamlProviderConfigsResponse, AuthError> {
306        let url = format!("{}/inboundSamlConfigs", self.base_url);
307        let mut url_obj = Url::parse(&url).map_err(|e| AuthError::ApiError(e.to_string()))?;
308
309        {
310            let mut query_pairs = url_obj.query_pairs_mut();
311            if let Some(max) = max_results {
312                query_pairs.append_pair("pageSize", &max.to_string());
313            }
314            if let Some(token) = page_token {
315                query_pairs.append_pair("pageToken", token);
316            }
317        }
318
319        let response = self.client.get(url_obj).send().await?;
320
321        if !response.status().is_success() {
322            let status = response.status();
323            let text = response.text().await.unwrap_or_default();
324            return Err(AuthError::ApiError(format!(
325                "List SAML configs failed {}: {}",
326                status, text
327            )));
328        }
329
330        let result: ListSamlProviderConfigsResponse = response.json().await?;
331        Ok(result)
332    }
333}