Skip to main content

bitwarden_organization_domains/
organization_domains_client.rs

1use std::sync::Arc;
2
3use bitwarden_core::{
4    ApiError, Client, FromClient, MissingFieldError, OrganizationId, client::ApiConfigurations,
5    require,
6};
7use bitwarden_error::bitwarden_error;
8use thiserror::Error;
9
10/// Errors returned from [`OrganizationDomainsClient`] operations.
11#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum OrganizationDomainsError {
14    /// A network request to the server failed.
15    #[error(transparent)]
16    Api(#[from] ApiError),
17    /// The server response was missing a required field.
18    #[error(transparent)]
19    MissingField(#[from] MissingFieldError),
20}
21
22/// Client for reading an organization's verified domains.
23#[bitwarden_ffi::wasm_object]
24#[derive(FromClient)]
25pub struct OrganizationDomainsClient {
26    pub(crate) api_configurations: Arc<ApiConfigurations>,
27}
28
29#[bitwarden_ffi::wasm_export]
30impl OrganizationDomainsClient {
31    /// Returns the names of every domain the organization has claimed and verified, for example
32    /// `example.com`.
33    ///
34    /// Domains that have been claimed but not yet verified are excluded, since the organization has
35    /// not proven ownership of them.
36    ///
37    /// Requires the Manage Users or Manage SSO permission. Prefer this over the full domains
38    /// endpoint when the DNS verification token and verification job metadata are not needed: the
39    /// full endpoint requires Manage SSO, and calling it without that permission returns a 401
40    /// that clients treat as an invalid access token, logging the user out.
41    pub async fn get_verified_domains(
42        &self,
43        organization_id: OrganizationId,
44    ) -> Result<Vec<String>, OrganizationDomainsError> {
45        let response = self
46            .api_configurations
47            .api_client
48            .organization_domain_api()
49            .get_all_mini(organization_id.into())
50            .await?;
51
52        require!(response.data)
53            .into_iter()
54            .filter(|domain| domain.verified_date.is_some())
55            .map(|domain| Ok(require!(domain.domain_name)))
56            .collect()
57    }
58}
59
60/// Extension trait for obtaining an [`OrganizationDomainsClient`] from a [`Client`].
61pub trait OrganizationDomainsClientExt {
62    /// Returns an [`OrganizationDomainsClient`]
63    fn organization_domains(&self) -> OrganizationDomainsClient;
64}
65
66impl OrganizationDomainsClientExt for Client {
67    fn organization_domains(&self) -> OrganizationDomainsClient {
68        OrganizationDomainsClient::from_client(self)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use bitwarden_api_api::{
75        apis::ApiClient,
76        models::{
77            OrganizationDomainMiniResponseModel,
78            OrganizationDomainMiniResponseModelListResponseModel,
79        },
80    };
81    use bitwarden_core::client::ApiConfigurations;
82
83    use super::*;
84
85    fn client_with(
86        response: OrganizationDomainMiniResponseModelListResponseModel,
87    ) -> OrganizationDomainsClient {
88        let api_client = ApiClient::new_mocked(move |mock| {
89            mock.organization_domain_api
90                .expect_get_all_mini()
91                .returning(move |_org| Ok(response.clone()))
92                .once();
93        });
94
95        OrganizationDomainsClient {
96            api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
97        }
98    }
99
100    #[tokio::test]
101    async fn get_verified_domains_excludes_unverified_domains() {
102        let client = client_with(OrganizationDomainMiniResponseModelListResponseModel {
103            object: Some("list".to_string()),
104            data: Some(vec![
105                OrganizationDomainMiniResponseModel {
106                    object: Some("organizationDomainMini".to_string()),
107                    domain_name: Some("verified.com".to_string()),
108                    verified_date: Some("2026-09-15T00:00:00Z".to_string()),
109                },
110                OrganizationDomainMiniResponseModel {
111                    object: Some("organizationDomainMini".to_string()),
112                    domain_name: Some("unverified.com".to_string()),
113                    verified_date: None,
114                },
115            ]),
116            continuation_token: None,
117        });
118
119        let domains = client
120            .get_verified_domains(OrganizationId::new_v4())
121            .await
122            .unwrap();
123
124        assert_eq!(domains, vec!["verified.com".to_string()]);
125    }
126
127    #[tokio::test]
128    async fn get_verified_domains_returns_empty_when_the_org_has_none() {
129        let client = client_with(OrganizationDomainMiniResponseModelListResponseModel {
130            object: Some("list".to_string()),
131            data: Some(vec![]),
132            continuation_token: None,
133        });
134
135        let domains = client
136            .get_verified_domains(OrganizationId::new_v4())
137            .await
138            .unwrap();
139
140        assert!(domains.is_empty());
141    }
142
143    #[tokio::test]
144    async fn get_verified_domains_errors_when_data_is_missing() {
145        let client = client_with(OrganizationDomainMiniResponseModelListResponseModel {
146            object: Some("list".to_string()),
147            data: None,
148            continuation_token: None,
149        });
150
151        let result = client.get_verified_domains(OrganizationId::new_v4()).await;
152
153        assert!(matches!(
154            result,
155            Err(OrganizationDomainsError::MissingField(_))
156        ));
157    }
158
159    #[tokio::test]
160    async fn get_verified_domains_errors_when_a_verified_domain_name_is_missing() {
161        let client = client_with(OrganizationDomainMiniResponseModelListResponseModel {
162            object: Some("list".to_string()),
163            data: Some(vec![OrganizationDomainMiniResponseModel {
164                object: Some("organizationDomainMini".to_string()),
165                domain_name: None,
166                verified_date: Some("2026-09-15T00:00:00Z".to_string()),
167            }]),
168            continuation_token: None,
169        });
170
171        let result = client.get_verified_domains(OrganizationId::new_v4()).await;
172
173        assert!(matches!(
174            result,
175            Err(OrganizationDomainsError::MissingField(_))
176        ));
177    }
178}