Skip to main content

bitwarden_encrypted_migrator/migrations/
account_recovery_v2_upgrade.rs

1//! Updates the account recovery keys of members who have upgraded to a V2 user key.
2//!
3//! A member upgrading to V2 leaves their organizations holding an account recovery key that still
4//! wraps their V1 user key. Only an owner or admin can replace it, so the migration runs for every
5//! organization the account administers.
6
7use 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    /// The organizations this account can update account recovery keys for.
51    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    /// A client holding an organization key for each of `organization_ids`, with
102    /// `organizations` in its state.
103    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    /// Answers `get_pending_v2_upgrades` for `organization_id` with no pending memberships,
142    /// exactly once.
143    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    /// The API mock fails any request for an organization it does not expect.
166    #[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        // The server rejects custom members, even with every account recovery permission.
171        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}