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