bitwarden_sm/
client_secrets.rs

1use bitwarden_core::Client;
2
3use crate::{
4    error::SecretsManagerError,
5    secrets::{
6        create_secret, delete_secrets, get_secret, get_secrets_by_ids, list_secrets,
7        list_secrets_by_project, sync_secrets, update_secret, SecretCreateRequest,
8        SecretGetRequest, SecretIdentifiersByProjectRequest, SecretIdentifiersRequest,
9        SecretIdentifiersResponse, SecretPutRequest, SecretResponse, SecretsDeleteRequest,
10        SecretsDeleteResponse, SecretsGetRequest, SecretsResponse, SecretsSyncRequest,
11        SecretsSyncResponse,
12    },
13};
14
15/// Aliases to maintain backward compatibility
16pub type ClientSecrets = SecretsClient;
17
18#[allow(missing_docs)]
19pub struct SecretsClient {
20    client: Client,
21}
22
23impl SecretsClient {
24    #[allow(missing_docs)]
25    pub fn new(client: Client) -> Self {
26        Self { client }
27    }
28
29    #[allow(missing_docs)]
30    pub async fn get(
31        &self,
32        input: &SecretGetRequest,
33    ) -> Result<SecretResponse, SecretsManagerError> {
34        get_secret(&self.client, input).await
35    }
36
37    #[allow(missing_docs)]
38    pub async fn get_by_ids(
39        &self,
40        input: SecretsGetRequest,
41    ) -> Result<SecretsResponse, SecretsManagerError> {
42        get_secrets_by_ids(&self.client, input).await
43    }
44
45    #[allow(missing_docs)]
46    pub async fn create(
47        &self,
48        input: &SecretCreateRequest,
49    ) -> Result<SecretResponse, SecretsManagerError> {
50        create_secret(&self.client, input).await
51    }
52
53    #[allow(missing_docs)]
54    pub async fn list(
55        &self,
56        input: &SecretIdentifiersRequest,
57    ) -> Result<SecretIdentifiersResponse, SecretsManagerError> {
58        list_secrets(&self.client, input).await
59    }
60
61    #[allow(missing_docs)]
62    pub async fn list_by_project(
63        &self,
64        input: &SecretIdentifiersByProjectRequest,
65    ) -> Result<SecretIdentifiersResponse, SecretsManagerError> {
66        list_secrets_by_project(&self.client, input).await
67    }
68
69    #[allow(missing_docs)]
70    pub async fn update(
71        &self,
72        input: &SecretPutRequest,
73    ) -> Result<SecretResponse, SecretsManagerError> {
74        update_secret(&self.client, input).await
75    }
76
77    #[allow(missing_docs)]
78    pub async fn delete(
79        &self,
80        input: SecretsDeleteRequest,
81    ) -> Result<SecretsDeleteResponse, SecretsManagerError> {
82        delete_secrets(&self.client, input).await
83    }
84
85    #[allow(missing_docs)]
86    pub async fn sync(
87        &self,
88        input: &SecretsSyncRequest,
89    ) -> Result<SecretsSyncResponse, SecretsManagerError> {
90        sync_secrets(&self.client, input).await
91    }
92}
93
94/// This trait is for backward compatibility
95pub trait ClientSecretsExt {
96    #[allow(missing_docs)]
97    fn secrets(&self) -> ClientSecrets;
98}
99
100impl ClientSecretsExt for Client {
101    fn secrets(&self) -> ClientSecrets {
102        SecretsClient::new(self.clone())
103    }
104}
105
106#[allow(missing_docs)]
107pub trait SecretsClientExt {
108    #[allow(missing_docs)]
109    fn secrets(&self) -> SecretsClient;
110}
111
112impl SecretsClientExt for Client {
113    fn secrets(&self) -> SecretsClient {
114        SecretsClient::new(self.clone())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use bitwarden_core::{
121        auth::login::AccessTokenLoginRequest, Client, ClientSettings, DeviceType,
122    };
123
124    use crate::{
125        secrets::{SecretGetRequest, SecretIdentifiersRequest},
126        ClientSecretsExt,
127    };
128
129    async fn start_mock(mocks: Vec<wiremock::Mock>) -> (wiremock::MockServer, Client) {
130        let server = wiremock::MockServer::start().await;
131
132        for mock in mocks {
133            server.register(mock).await;
134        }
135
136        let settings = ClientSettings {
137            identity_url: format!("http://{}/identity", server.address()),
138            api_url: format!("http://{}/api", server.address()),
139            user_agent: "Bitwarden Rust-SDK [TEST]".into(),
140            device_type: DeviceType::SDK,
141        };
142
143        (server, Client::new(Some(settings)))
144    }
145
146    #[tokio::test]
147    async fn test_access_token_login() {
148        use wiremock::{matchers, Mock, ResponseTemplate};
149
150        // Create the mock server with the necessary routes for this test
151        let (_server, client) = start_mock(vec![
152            Mock::given(matchers::path("/identity/connect/token"))
153            .respond_with(ResponseTemplate::new(200).set_body_json(
154                serde_json::json!({
155                    "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQzg1REJEOUFSUzI1NiIsInR5cCI6\
156                    ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJuYmYiOjE2NzUxMDM3ODEsImV4cCI6MTY3NTEwNzM4MSwiaXNzIjo\
157                    iaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI6ImVjMmMxZDQ2LTZhNGItNDc1MS1hMzEwLWFmOTYwMTMxN2YyZCIsInN1YiI6ImQzNDgwNGNhLTR\
158                    mNmMtNDM5Mi04NmI3LWFmOTYwMTMxNzVkMCIsIm9yZ2FuaXphdGlvbiI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImp0aSI\
159                    6IjU3QUU0NzQ0MzIwNzk1RThGQkQ4MUIxNDA2RDQyNTQyIiwiaWF0IjoxNjc1MTAzNzgxLCJzY29wZSI6WyJhcGkuc2VjcmV0cyJdfQ.GRKYzqgJZHEE\
160                    ZHsJkhVZH8zjYhY3hUvM4rhdV3FU10WlCteZdKHrPIadCUh-Oz9DxIAA2HfALLhj1chL4JgwPmZgPcVS2G8gk8XeBmZXowpVWJ11TXS1gYrM9syXbv9j\
161                    0JUCdpeshH7e56WnlpVynyUwIum9hmYGZ_XJUfmGtlKLuNjYnawTwLEeR005uEjxq3qI1kti-WFnw8ciL4a6HLNulgiFw1dAvs4c7J0souShMfrnFO3g\
162                    SOHff5kKD3hBB9ynDBnJQSFYJ7dFWHIjhqs0Vj-9h0yXXCcHvu7dVGpaiNjNPxbh6YeXnY6UWcmHLDtFYsG2BWcNvVD4-VgGxXt3cMhrn7l3fSYuo32Z\
163                    Yk4Wop73XuxqF2fmfmBdZqGI1BafhENCcZw_bpPSfK2uHipfztrgYnrzwvzedz0rjFKbhDyrjzuRauX5dqVJ4ntPeT9g_I5n71gLxiP7eClyAx5RxdF6\
164                    He87NwC8i-hLBhugIvLTiDj-Sk9HvMth6zaD0ebxd56wDjq8-CMG_WcgusDqNzKFHqWNDHBXt8MLeTgZAR2rQMIMFZqFgsJlRflbig8YewmNUA9wAU74\
165                    TfxLY1foO7Xpg49vceB7C-PlvGi1VtX6F2i0tc_67lA5kWXnnKBPBUyspoIrmAUCwfms5nTTqA9xXAojMhRHAos_OdM",
166                    "expires_in":3600,
167                    "token_type":"Bearer",
168                    "scope":"api.secrets",
169                    "encrypted_payload":"2.E9fE8+M/VWMfhhim1KlCbQ==|eLsHR484S/tJbIkM6spnG/HP65tj9A6Tba7kAAvUp+rYuQmGLixiOCfMsqt5OvBctDfvvr/Aes\
170                    Bu7cZimPLyOEhqEAjn52jF0eaI38XZfeOG2VJl0LOf60Wkfh3ryAMvfvLj3G4ZCNYU8sNgoC2+IQ==|lNApuCQ4Pyakfo/wwuuajWNaEX/2MW8/3rjXB/V7n+k="})
171            )),
172            Mock::given(matchers::path("/api/organizations/f4e44a7f-1190-432a-9d4a-af96013127cb/secrets"))
173            .respond_with(ResponseTemplate::new(200).set_body_json(
174                serde_json::json!({
175                    "secrets":[{
176                            "id":"15744a66-341a-4c62-af50-af960166b6bc",
177                            "organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb",
178                            "key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=",
179                            "creationDate":"2023-01-26T21:46:02.2182556Z",
180                            "revisionDate":"2023-01-26T21:46:02.2182557Z"
181                    }],
182                    "projects":[],
183                    "object":"SecretsWithProjectsList"
184                })
185            )),
186            Mock::given(matchers::path("/api/secrets/15744a66-341a-4c62-af50-af960166b6bc"))
187            .respond_with(ResponseTemplate::new(200).set_body_json(
188                serde_json::json!({
189                    "id":"15744a66-341a-4c62-af50-af960166b6bc",
190                    "organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb",
191                    "key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=",
192                    "value":"2.Gl34n9JYABC7V21qHcBzHg==|c1Ds244pob7i+8+MXe4++w==|Shimz/qKMYZmzSFWdeBzFb9dFz7oF6Uv9oqkws7rEe0=",
193                    "note":"2.Cn9ABJy7+WfR4uUHwdYepg==|+nbJyU/6hSknoa5dcEJEUg==|1DTp/ZbwGO3L3RN+VMsCHz8XDr8egn/M5iSitGGysPA=",
194                    "creationDate":"2023-01-26T21:46:02.2182556Z",
195                    "revisionDate":"2023-01-26T21:46:02.2182557Z",
196                    "object":"secret"
197                })
198            ))
199        ]).await;
200
201        // Test the login is correct and we store the returned organization ID correctly
202        let res = client
203            .auth()
204            .login_access_token(&AccessTokenLoginRequest {
205                access_token: "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==".into(),
206                state_file: None,
207            })
208            .await
209            .unwrap();
210        assert!(res.authenticated);
211
212        let organization_id = "f4e44a7f-1190-432a-9d4a-af96013127cb".try_into().unwrap();
213
214        // Test that we can retrieve the list of secrets correctly
215        let mut res = client
216            .secrets()
217            .list(&SecretIdentifiersRequest { organization_id })
218            .await
219            .unwrap();
220        assert_eq!(res.data.len(), 1);
221
222        // Test that given a secret ID we can get it's data
223        let res = client
224            .secrets()
225            .get(&SecretGetRequest {
226                id: res.data.remove(0).id,
227            })
228            .await
229            .unwrap();
230        assert_eq!(res.key, "TEST");
231        assert_eq!(res.note, "TEST");
232        assert_eq!(res.value, "TEST");
233    }
234}