bitwarden_encrypted_migrator/migrations/
account_recovery_v2_upgrade.rs1use bitwarden_core::{Client, FromClient, OrganizationId};
8use bitwarden_member_administration::OrganizationUsersManagementClient;
9use bitwarden_organizations::{OrganizationsClient, ProfileOrganization};
10use tracing::warn;
11
12pub(crate) struct AccountRecoveryV2UpgradeMigration {
13 organizations: OrganizationsClient,
14 organization_users_management: OrganizationUsersManagementClient,
15}
16
17impl FromClient for AccountRecoveryV2UpgradeMigration {
18 fn from_client(client: &Client) -> Self {
19 Self {
20 organizations: OrganizationsClient::from_client(client),
21 organization_users_management: OrganizationUsersManagementClient::from_client(client),
22 }
23 }
24}
25
26impl AccountRecoveryV2UpgradeMigration {
27 pub(crate) async fn run(&self) {
28 let organizations = match self.organizations.get_all().await {
29 Ok(organizations) => organizations,
30 Err(e) => {
31 warn!("Failed to read the organizations: {e}");
32 return;
33 }
34 };
35
36 for organization_id in self.organizations_to_upgrade(&organizations) {
37 if let Err(e) = self
38 .organization_users_management
39 .upgrade_pending_account_recovery_keys(organization_id)
40 .await
41 {
42 warn!(
43 %organization_id,
44 "Failed to upgrade pending account recovery keys: {e}"
45 );
46 }
47 }
48 }
49
50 fn organizations_to_upgrade(
52 &self,
53 organizations: &[ProfileOrganization],
54 ) -> Vec<OrganizationId> {
55 organizations
56 .iter()
57 .filter(|o| {
58 self.organization_users_management
59 .can_administer_account_recovery_keys(o.details.id, &o.membership.r#type)
60 })
61 .map(|o| o.details.id)
62 .collect()
63 }
64}
65
66#[cfg(test)]
67pub(crate) mod tests {
68 use std::sync::Arc;
69
70 use bitwarden_api_api::{
71 apis::{ApiClient, ApiClientMock},
72 models::OrganizationUserPendingV2UpgradeResponseModelListResponseModel,
73 };
74 use bitwarden_core::{
75 ClientBuilder, client::ApiConfigurations, key_management::SymmetricKeySlotId,
76 };
77 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
78 use bitwarden_organizations::{
79 OrganizationDetails, OrganizationMembership, OrganizationUserType, Permissions,
80 };
81 use bitwarden_state::repository::Repository;
82 use bitwarden_test::MemoryRepository;
83
84 use super::*;
85
86 pub(crate) fn organization(user_type: OrganizationUserType) -> ProfileOrganization {
87 let default = ProfileOrganization::default();
88 ProfileOrganization {
89 details: OrganizationDetails {
90 id: OrganizationId::new_v4(),
91 ..default.details
92 },
93 membership: OrganizationMembership {
94 r#type: user_type,
95 ..default.membership
96 },
97 ..ProfileOrganization::default()
98 }
99 }
100
101 pub(crate) async fn client(
104 organizations: &[ProfileOrganization],
105 organization_ids: &[OrganizationId],
106 configure: impl FnOnce(&mut ApiClientMock),
107 ) -> Client {
108 let client = ClientBuilder::new()
109 .with_api_configurations(Arc::new(ApiConfigurations::from_api_client(
110 ApiClient::new_mocked(configure),
111 )))
112 .build();
113
114 let repository = Arc::new(MemoryRepository::<ProfileOrganization>::default());
115 for organization in organizations {
116 repository
117 .set(organization.details.id, organization.clone())
118 .await
119 .expect("the repository is in memory");
120 }
121 client
122 .platform()
123 .state()
124 .register_client_managed(repository);
125
126 let key_store = client.internal.get_key_store();
127 let mut ctx = key_store.context_mut();
128 for organization_id in organization_ids {
129 #[allow(deprecated)]
130 ctx.set_symmetric_key(
131 SymmetricKeySlotId::Organization(*organization_id),
132 SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
133 )
134 .expect("the context is mutable");
135 }
136 drop(ctx);
137
138 client
139 }
140
141 pub(crate) fn expect_no_pending(mock: &mut ApiClientMock, organization_id: OrganizationId) {
144 mock.organization_users_keys_api
145 .expect_get_pending_v2_upgrades()
146 .withf(move |org, _token| OrganizationId::new(*org) == organization_id)
147 .returning(|_org, _token| {
148 Ok(
149 OrganizationUserPendingV2UpgradeResponseModelListResponseModel {
150 object: None,
151 data: Some(vec![]),
152 continuation_token: None,
153 },
154 )
155 })
156 .once();
157 }
158
159 async fn run(client: &Client) {
160 AccountRecoveryV2UpgradeMigration::from_client(client)
161 .run()
162 .await;
163 }
164
165 #[tokio::test]
167 async fn test_only_owner_and_admin_memberships_are_upgraded() {
168 let owner = organization(OrganizationUserType::Owner);
169 let admin = organization(OrganizationUserType::Admin);
170 let mut custom = organization(OrganizationUserType::Custom);
172 custom.membership.permissions = Permissions {
173 manage_reset_password: true,
174 manage_users: true,
175 ..custom.membership.permissions
176 };
177 let member = organization(OrganizationUserType::User);
178 let (owner_id, admin_id) = (owner.details.id, admin.details.id);
179 let all = [owner, admin, custom, member];
180 let ids: Vec<_> = all.iter().map(|o| o.details.id).collect();
181
182 let client = client(&all, &ids, |mock| {
183 expect_no_pending(mock, owner_id);
184 expect_no_pending(mock, admin_id);
185 })
186 .await;
187
188 run(&client).await;
189 }
190
191 #[tokio::test]
192 async fn test_memberships_without_an_organization_key_are_skipped() {
193 let locked = organization(OrganizationUserType::Owner);
194 let unlocked = organization(OrganizationUserType::Owner);
195 let unlocked_id = unlocked.details.id;
196
197 let client = client(&[locked, unlocked], &[unlocked_id], |mock| {
198 expect_no_pending(mock, unlocked_id);
199 })
200 .await;
201
202 run(&client).await;
203 }
204
205 #[tokio::test]
206 async fn test_a_failed_upgrade_does_not_stop_the_other_organizations() {
207 let failing = organization(OrganizationUserType::Owner);
208 let succeeding = organization(OrganizationUserType::Admin);
209 let (failing_id, succeeding_id) = (failing.details.id, succeeding.details.id);
210
211 let client = client(
212 &[failing, succeeding],
213 &[failing_id, succeeding_id],
214 |mock| {
215 mock.organization_users_keys_api
216 .expect_get_pending_v2_upgrades()
217 .withf(move |org, _token| OrganizationId::new(*org) == failing_id)
218 .returning(|_org, _token| {
219 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
220 "connection reset",
221 )))
222 })
223 .once();
224 expect_no_pending(mock, succeeding_id);
225 },
226 )
227 .await;
228
229 run(&client).await;
230 }
231
232 #[tokio::test]
233 async fn test_no_organizations_upgrades_nothing() {
234 let client = client(&[], &[], |_| {}).await;
235
236 run(&client).await;
237 }
238
239 #[tokio::test]
240 async fn test_no_organization_repository_upgrades_nothing() {
241 let client = Client::new(None);
242
243 run(&client).await;
244 }
245}