diff --git a/src/Api/c:/bwtest/licenses/organization/617dab3c-117b-4d59-8b51-b37501587c1e.json b/src/Api/c:/bwtest/licenses/organization/617dab3c-117b-4d59-8b51-b37501587c1e.json new file mode 100644 index 000000000000..7f871fc39352 --- /dev/null +++ b/src/Api/c:/bwtest/licenses/organization/617dab3c-117b-4d59-8b51-b37501587c1e.json @@ -0,0 +1,48 @@ +{ + "LicenseKey": "eBOUhz56niomM78ypqFG", + "InstallationId": "cfc6a734-4c6c-4ba5-b639-b28700af2e81", + "Id": "1b313b3f-2944-4b23-b1e6-b3750155481b", + "Name": "test", + "BillingEmail": "mzieniuk\u002Btest@bitwarden.com", + "BusinessName": null, + "Enabled": true, + "Plan": "Enterprise (Annually)", + "PlanType": 20, + "Seats": 5, + "MaxCollections": null, + "UsePolicies": true, + "UseSso": true, + "UseKeyConnector": true, + "UseScim": true, + "UseGroups": true, + "UseEvents": true, + "UseDirectory": true, + "UseTotp": true, + "Use2fa": true, + "UseApi": true, + "UseResetPassword": true, + "MaxStorageGb": 1, + "SelfHost": true, + "UsersGetPremium": true, + "UseCustomPermissions": true, + "Version": 15, + "Issued": "2025-11-14T21:58:44.590207Z", + "Refresh": "2025-12-14T21:58:44.590207Z", + "Expires": "2026-12-19T20:43:19Z", + "ExpirationWithoutGracePeriod": "2026-10-20T20:43:19Z", + "UsePasswordManager": true, + "UseSecretsManager": false, + "SmSeats": null, + "SmServiceAccounts": null, + "UseRiskInsights": false, + "LimitCollectionCreationDeletion": false, + "AllowAdminAccessToAllCollectionItems": false, + "Trial": false, + "LicenseType": 1, + "UseOrganizationDomains": true, + "UseAdminSponsoredFamilies": false, + "UseAutomaticUserConfirmation": false, + "Hash": "o3KjvtEPiv2oBeJKj9JKxUakIZDfP41vOqoA0kS3mlY=", + "Signature": "gDJ4lSlOCgiJMKrUiESQW1cdkkzAcN3J/9cH3ayXD78sE3llCWA41RjZgbIAryseTB7DU2voD0VJt2pXY6G5\u002B0AEY72V\u002BHVTBECY83HLQmb12KPYU6Q/Pv/5Q6Tv20e\u002BwVgv\u002BuCioq5iBliRxyTDEISkih4Dafegs7uAG02McB18mTUzuTX19jPBGLHPVJJ8V2qskMBvu1YoG67gv51EFZHZNg\u002BhFiWGHQ15NkqEaN/sD3AAVDQmG9DzXOMaynPbjy4wgeHOvUdb4glUYkrDsdXrDlw3z0nl1L2NBnxrdfNpebWbtMBH50\u002BudT9FJXEq7oP5Pyo7qVtPIX\u002BcPmi2\u002BrLSF8jE5JorfcDXIrg4Hzh\u002BefZzlJXmpz2J5MAsic0drQ1sO4rWN8WAupil/mRHk3zfyzbNY39s3vTqWLghUWsuZ0URWngs0A9BIHus/EioYieEvkq4ziug\u002BCMPOzTKDB61jJG2hDaLD1VXqRPonpX4KnfI/0NKw\u002BOL\u002BRpNdkLyqhR0wREZdjBl41AvBAKu8/bMeBoQntWESheJnrJJ2AY/zAsTL0EWxWqS39wn7/qxRQ/s0Agb2u5hsYDSKAmXPKajliVjaLFQ8pluvSlB7M0zuKzxlSgNOT2JyBCdZTv7pofFkbkWA7dKbhnOWWwk/8WmZHv/6YnOor97wgPKO20=", + "Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJMaWNlbnNlVHlwZSI6Ik9yZ2FuaXphdGlvbiIsIklkIjoiMWIzMTNiM2YtMjk0NC00YjIzLWIxZTYtYjM3NTAxNTU0ODFiIiwiRW5hYmxlZCI6IlRydWUiLCJQbGFuVHlwZSI6IkVudGVycHJpc2VBbm51YWxseSIsIlVzZVBvbGljaWVzIjoiVHJ1ZSIsIlVzZVNzbyI6IlRydWUiLCJVc2VLZXlDb25uZWN0b3IiOiJUcnVlIiwiVXNlU2NpbSI6IlRydWUiLCJVc2VHcm91cHMiOiJUcnVlIiwiVXNlRXZlbnRzIjoiVHJ1ZSIsIlVzZURpcmVjdG9yeSI6IlRydWUiLCJVc2VUb3RwIjoiVHJ1ZSIsIlVzZTJmYSI6IlRydWUiLCJVc2VBcGkiOiJUcnVlIiwiVXNlUmVzZXRQYXNzd29yZCI6IlRydWUiLCJTZWxmSG9zdCI6IlRydWUiLCJVc2Vyc0dldFByZW1pdW0iOiJUcnVlIiwiVXNlQ3VzdG9tUGVybWlzc2lvbnMiOiJUcnVlIiwiVXNlUGFzc3dvcmRNYW5hZ2VyIjoiVHJ1ZSIsIlVzZVNlY3JldHNNYW5hZ2VyIjoiRmFsc2UiLCJMaW1pdENvbGxlY3Rpb25DcmVhdGlvbkRlbGV0aW9uIjoiRmFsc2UiLCJBbGxvd0FkbWluQWNjZXNzVG9BbGxDb2xsZWN0aW9uSXRlbXMiOiJGYWxzZSIsIlVzZVJpc2tJbnNpZ2h0cyI6IkZhbHNlIiwiSXNzdWVkIjoiMTEvMTQvMjAyNSAyMTo1ODo0NCIsIkV4cGlyZXMiOiIxMi8xOS8yMDI2IDIwOjQzOjE5IiwiUmVmcmVzaCI6IjEyLzE0LzIwMjUgMjE6NTg6NDQiLCJUcmlhbCI6IkZhbHNlIiwiVXNlQWRtaW5TcG9uc29yZWRGYW1pbGllcyI6IkZhbHNlIiwiVXNlT3JnYW5pemF0aW9uRG9tYWlucyI6IlRydWUiLCJVc2VBdXRvbWF0aWNVc2VyQ29uZmlybWF0aW9uIjoiRmFsc2UiLCJOYW1lIjoidGVzdCIsIkJpbGxpbmdFbWFpbCI6Im16aWVuaXVrK3Rlc3RAYml0d2FyZGVuLmNvbSIsIlBsYW4iOiJFbnRlcnByaXNlIChBbm51YWxseSkiLCJMaWNlbnNlS2V5IjoiZUJPVWh6NTZuaW9tTTc4eXBxRkciLCJJbnN0YWxsYXRpb25JZCI6ImNmYzZhNzM0LTRjNmMtNGJhNS1iNjM5LWIyODcwMGFmMmU4MSIsIlNlYXRzIjoiNSIsIk1heFN0b3JhZ2VHYiI6IjEiLCJFeHBpcmF0aW9uV2l0aG91dEdyYWNlUGVyaW9kIjoiMTAvMjAvMjAyNiAyMDo0MzoxOSIsImp0aSI6IjdkNzY0NDAwLTMxODQtNDA5OC1hNzA3LTRiMDVmODIxMjNhNyIsIm5iZiI6MTc2MzE1NzUyNCwiZXhwIjoxNzk0NjkzNTI0LCJpYXQiOjE3NjMxNTc1MjQsImlzcyI6ImJpdHdhcmRlbiIsImF1ZCI6Im9yZ2FuaXphdGlvbjoxYjMxM2IzZi0yOTQ0LTRiMjMtYjFlNi1iMzc1MDE1NTQ4MWIifQ.LwnYLYB6jOPe2Aj8GNfm1X9TnAys9URsOXHQB2zQbK0H3aaDmP7PjGzPssUde4t6IPfnvmeYC_MHW61JmSiFejXOxyNX9CDpN8EtIt0g9an18rFoW7CicJGHM06uXGhbfIyqjqt91NdOkTOc5sQBC_YYvwQsTbJ-n91hDIuGZYkx58BZpQGtkr8tu5MiigRjqZmsQa1_zD7SoJ2Qi8ZZTq5-HLNE7t_d__WaBD2WYamLuBwOHZQcep3UmRSmpif9gOR4iu629PTfDr1n7NCt4KDXzlAzvm-xO4Q1sGf36RprqXDLy4EhyyCMGfaCodiN_UxrHdZJgMmF9Dvfwu8ZND_hmdI4ZIEqi6H2IU5KDD1fVdhp6sbA_GOccnEMY_nS4q0Pbe8wovuuA6kSHxoQf5052LSteFexzM9ketLSZoD8x0XiTjk4i5zkW2r4Ug0ozgkXjqXTbuhSz4jNfBs3aW8ZLIpRD0FoX0JAcyqmmh09S3DBK2VMGawvATswSXG0LZQfXM7ZB5CvEHW0W_BaCx4UOaw9v00u4BHallu6DUWZvhosjzhcx0c4jdPz3rnQ50B9ylTrc6yL-977QIen2w2_tguZFi_1meJOQ6ysTAU-G18KnTU2rM-dlqPRzHXAx_ltqacAXCE6vp83RCvQoeaffzV7O67Dcp8ion8kN0o" +} \ No newline at end of file diff --git a/src/Api/c:/bwtest/licenses/organization/84139cad-dab3-4e8d-853c-b292000b57ca.json b/src/Api/c:/bwtest/licenses/organization/84139cad-dab3-4e8d-853c-b292000b57ca.json new file mode 100644 index 000000000000..d8196e6a9237 --- /dev/null +++ b/src/Api/c:/bwtest/licenses/organization/84139cad-dab3-4e8d-853c-b292000b57ca.json @@ -0,0 +1,47 @@ +{ + "LicenseKey": "YxQAIFm9OyRUSqmWHL2M", + "InstallationId": "cfc6a734-4c6c-4ba5-b639-b28700af2e81", + "Id": "3369f65c-9894-47b1-a5d6-b19300a5ea0a", + "Name": "test", + "BillingEmail": "mzieniuk\u002Btest@bitwarden.com", + "BusinessName": null, + "Enabled": true, + "Plan": "Enterprise (Monthly)", + "PlanType": 19, + "Seats": 2, + "MaxCollections": null, + "UsePolicies": true, + "UseSso": true, + "UseKeyConnector": true, + "UseScim": true, + "UseGroups": true, + "UseEvents": true, + "UseDirectory": true, + "UseTotp": true, + "Use2fa": true, + "UseApi": true, + "UseResetPassword": true, + "MaxStorageGb": 1, + "SelfHost": true, + "UsersGetPremium": true, + "UseCustomPermissions": true, + "Version": 15, + "Issued": "2025-09-08T17:54:44.254856Z", + "Refresh": "2024-06-25T10:04:05Z", + "Expires": "2024-06-25T10:04:05Z", + "ExpirationWithoutGracePeriod": null, + "UsePasswordManager": true, + "UseSecretsManager": true, + "SmSeats": 2, + "SmServiceAccounts": 50, + "UseRiskInsights": false, + "LimitCollectionCreationDeletion": false, + "AllowAdminAccessToAllCollectionItems": true, + "Trial": false, + "LicenseType": 1, + "UseOrganizationDomains": true, + "UseAdminSponsoredFamilies": false, + "Hash": "QeaSbSnqHVNP7IXKyNiuKuFXLdRE\u002BNplyjtNJCXGz8A=", + "Signature": "qRyQZASV6oIW9igBHZbTA55dS0/ddRoY2trBKNTt240xT55Y7w1mIe7zmGjbC4ORo61/1wjF3qlmHmlpEpAwt6EaalrWF3LA00xN3Ntd5k0MM8dRTV3XzKVxDc35BCaZxES7njvHrcLTWk7z/CWs66dZqqD\u002BgP/yiEHuzngWL1bS\u002BonCJx1K4MRj7G80TJXBD1/69iZC3E/W\u002BWyMMElR0Wg2oubGd1UxIf\u002B21C\u002BCKAgOdYqaV62gJHDxpSDECQDCtKM860Db9EcPT26US1oxySvAFAi3\u002B65PXR4iddFdP42UGPR3w3xFa4MvLL1rLptaq325K\u002Bd3oSbSFZJOwWJnteiZFnjTnAtvqUF6IkgNjLV9YCqdUCmEbxAb/fZ5oOJtja3IJkJ95VVcaJWGMxsM7o72s/UqVMRsmLoqef31D\u002BgFzhkMGg\u002BeW3i9oUmQCdDEz6yUBG5X1TiI5u8ldnns0u4lHjLQXJZVaXWvvH/i6vR1rhY4UHIO0zA7FxCBZo\u002BJa2LvPcajry13nZXjZ9t1GEttNx5qKyw3guU3cyOoJPnEHayXWwIdruGOhDrt7HjlRjBNjRVRVA3B5wBIz3mbt2e8WOCwdBX\u002BF18D7SXD3RsQmdvTkBFE9W33Flm4oDMwjIjC6FbgG70oUynJEftCLUnA5KCyfA1nKvsV10qwVe0=", + "Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJMaWNlbnNlVHlwZSI6Ik9yZ2FuaXphdGlvbiIsIklkIjoiMzM2OWY2NWMtOTg5NC00N2IxLWE1ZDYtYjE5MzAwYTVlYTBhIiwiRW5hYmxlZCI6IlRydWUiLCJQbGFuVHlwZSI6IkVudGVycHJpc2VNb250aGx5IiwiVXNlUG9saWNpZXMiOiJUcnVlIiwiVXNlU3NvIjoiVHJ1ZSIsIlVzZUtleUNvbm5lY3RvciI6IlRydWUiLCJVc2VTY2ltIjoiVHJ1ZSIsIlVzZUdyb3VwcyI6IlRydWUiLCJVc2VFdmVudHMiOiJUcnVlIiwiVXNlRGlyZWN0b3J5IjoiVHJ1ZSIsIlVzZVRvdHAiOiJUcnVlIiwiVXNlMmZhIjoiVHJ1ZSIsIlVzZUFwaSI6IlRydWUiLCJVc2VSZXNldFBhc3N3b3JkIjoiVHJ1ZSIsIlNlbGZIb3N0IjoiVHJ1ZSIsIlVzZXJzR2V0UHJlbWl1bSI6IlRydWUiLCJVc2VDdXN0b21QZXJtaXNzaW9ucyI6IlRydWUiLCJVc2VQYXNzd29yZE1hbmFnZXIiOiJUcnVlIiwiVXNlU2VjcmV0c01hbmFnZXIiOiJUcnVlIiwiTGltaXRDb2xsZWN0aW9uQ3JlYXRpb25EZWxldGlvbiI6IkZhbHNlIiwiQWxsb3dBZG1pbkFjY2Vzc1RvQWxsQ29sbGVjdGlvbkl0ZW1zIjoiVHJ1ZSIsIlVzZVJpc2tJbnNpZ2h0cyI6IkZhbHNlIiwiSXNzdWVkIjoiMDkvMDgvMjAyNSAxNzo1NDo0NCIsIkV4cGlyZXMiOiIwNi8yNS8yMDI0IDEwOjA0OjA1IiwiUmVmcmVzaCI6IjA2LzI1LzIwMjQgMTA6MDQ6MDUiLCJFeHBpcmF0aW9uV2l0aG91dEdyYWNlUGVyaW9kIjoiMDYvMjUvMjAyNCAxMDowNDowNSIsIlRyaWFsIjoiRmFsc2UiLCJVc2VBZG1pblNwb25zb3JlZEZhbWlsaWVzIjpbIkZhbHNlIiwiRmFsc2UiXSwiVXNlT3JnYW5pemF0aW9uRG9tYWlucyI6IlRydWUiLCJOYW1lIjoidGVzdCIsIkJpbGxpbmdFbWFpbCI6Im16aWVuaXVrK3Rlc3RAYml0d2FyZGVuLmNvbSIsIlBsYW4iOiJFbnRlcnByaXNlIChNb250aGx5KSIsIkxpY2Vuc2VLZXkiOiJZeFFBSUZtOU95UlVTcW1XSEwyTSIsIkluc3RhbGxhdGlvbklkIjoiY2ZjNmE3MzQtNGM2Yy00YmE1LWI2MzktYjI4NzAwYWYyZTgxIiwiU2VhdHMiOiIyIiwiTWF4U3RvcmFnZUdiIjoiMSIsIlNtU2VhdHMiOiIyIiwiU21TZXJ2aWNlQWNjb3VudHMiOiI1MCIsImp0aSI6IjMzOTg1ODQ4LTg5MzUtNGUwMS1hYTI2LTk0YjdjYjdlNDQ3NCIsIm5iZiI6MTc1NzM1NDA4NCwiZXhwIjoxNzg4ODkwMDg0LCJpYXQiOjE3NTczNTQwODQsImlzcyI6ImJpdHdhcmRlbiIsImF1ZCI6Im9yZ2FuaXphdGlvbjozMzY5ZjY1Yy05ODk0LTQ3YjEtYTVkNi1iMTkzMDBhNWVhMGEifQ.j3VZADaRIR-exnuPLemakInixRTWslAhEAgAoGQ8XufNsSyyDDx4LEgg0-9JmvCJ8FAY_am6MxWS6UDN6cq3uV49nLuTYrUvUKPfbNiHsqi39BekdZE7uJo_uz8cpL71bNOYcoUV0YTBBrRAgtT-SoVlyCFwW1JwzfypYWg1R9fnP_EPOLDuZuC9Nd0S1Z-bERVLfLYJ6I46JzMM66QGpGFbOkE8laCgYTkw23pFHLehbVvFNxhkc7FHusBHiG04UNQUbpZ-s-r938bhbHSZS6RJ9-vkCPGQ85DJbYvN4M3fIo2uCSw7_n9dLYTeQ47X0ScD302lElW-8XzZeVGPj7RHEemDNiL7QK190Vc7bfGk4_yL11pisBtnKDea2B1UpBwpV3fRPDRKZ7ViVFgLv6NXmtVwyO5Hr9r0AhJUNRPtyolmzNofFJFc4IdUpVIINRJSXqlO0qCAIRQGFiJyFZSEXSW1MT8mAnMUY0Zi0UvonQS8VRBBrzlFS-kHrzrd2Q8aBitgy2fU2XDjIYGAiV2qbY1DYKP1tc09Xo1U-PBL9rMAn34QcCE06wExS_MKEB8M6pGOg1vjVMAYpZGZSwnKd3dqxzFO7zykYX2CC10Oo6u0l9HDdgCNh197WBuJHoPYwIva9yWqFY5LECt6W8Kkf9mHBBEeRHCLsi6ar9Q" +} \ No newline at end of file diff --git a/src/Api/c:/bwtest/licenses/organization/dafdba15-0b7f-42ba-aaa4-b4440161fe29.json b/src/Api/c:/bwtest/licenses/organization/dafdba15-0b7f-42ba-aaa4-b4440161fe29.json new file mode 100644 index 000000000000..658b83f60052 --- /dev/null +++ b/src/Api/c:/bwtest/licenses/organization/dafdba15-0b7f-42ba-aaa4-b4440161fe29.json @@ -0,0 +1,52 @@ +{ + "LicenseKey": "yf6CWvo4thy91MzyGJBx", + "InstallationId": "cfc6a734-4c6c-4ba5-b639-b28700af2e81", + "Id": "5ae45e98-9bd6-477e-97b6-b4440153c30e", + "Name": "test", + "BillingEmail": "m2@b.com", + "BusinessName": "", + "Enabled": true, + "Plan": "Enterprise (Annually)", + "PlanType": 20, + "Seats": 10, + "MaxCollections": null, + "UsePolicies": true, + "UseSso": true, + "UseKeyConnector": true, + "UseScim": true, + "UseGroups": true, + "UseEvents": true, + "UseDirectory": true, + "UseTotp": true, + "Use2fa": true, + "UseApi": true, + "UseResetPassword": true, + "MaxStorageGb": 5, + "SelfHost": true, + "UsersGetPremium": true, + "UseCustomPermissions": true, + "Version": 15, + "Issued": "2026-05-14T18:45:56", + "Refresh": "2026-05-15T20:37:03", + "Expires": "2026-05-15T20:37:03", + "ExpirationWithoutGracePeriod": null, + "UsePasswordManager": true, + "UseSecretsManager": false, + "SmSeats": null, + "SmServiceAccounts": null, + "UseRiskInsights": false, + "UsePhishingBlocker": false, + "LimitCollectionCreationDeletion": false, + "AllowAdminAccessToAllCollectionItems": false, + "Trial": true, + "LicenseType": 1, + "UseOrganizationDomains": true, + "UseAdminSponsoredFamilies": false, + "UseAutomaticUserConfirmation": false, + "UseDisableSmAdsForUsers": false, + "UseMyItems": true, + "UseInviteLinks": true, + "Hash": "o6BIQlKzgtsk34kdF7r\u002BmeMYvxTDY3YpDhipuuf/aEc=", + "Signature": "VjQy9i11QPJMxjGm8L5byAFBUYFqK/ytlfh53UoQDgXi\u002BMC5U73djwckWlkADdX9h/RosiMLCfIx\u002BrHB19F957ksKwQoTL0\u002BpsVa40eS4wvaUUfqQHfAAkU/VXpbw\u002BW\u002BD9PlxF7X6YVuFyZoQ6ZZiudev3tGpAfaeHoHIuyJYz0\u002Bzt2xJzt8xeEjihh85l6/dyxH0ex4s9R6ZyZyOjgnzKYk85Wg50\u002B5oZncBd6iXrxSzmHz7NCIGtvHwD3My7x4wyzWZ\u002BH\u002BLk5ADd3J17ArdpklZn9jjAPI760dleocxP0ck/PjGDgj9nZfd0jARA5OgaJYReasgNwUhMh8p5WdOsaOh9omkE1Kx1m5/WEb49SEy3EACoG3UqniLmc\u002BrkF6LXOZHtfVr3yFRsNOLnYMdGQq7j8Wv7yaXyQQQRoGbgZodwhzOQ9e5aKjAO0Y1CTiMBmvGlNgXV1A6HadIIJqQzL13IUijMppIE1pscLQEiToW3ca3tsMrf04npxWBl2Yq/2TrySEhHQao9hr5hX1D0Us/6uPOJGAqYKzZq/pP1ybLdg4dgJJX8UsNTWaIeNzMGvlsdcxZjf7vVDtfjYwzlzDKFrGNdLBgRt4vhTBrvW2PLGKN3KUos3dXnRlVL5iDRt5efiFmaEFgeKUq\u002BlbAs8L9Y8G/Ouy37jMgbRv7SM=", + "Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJMaWNlbnNlVHlwZSI6Ik9yZ2FuaXphdGlvbiIsIklkIjoiNWFlNDVlOTgtOWJkNi00NzdlLTk3YjYtYjQ0NDAxNTNjMzBlIiwiRW5hYmxlZCI6IlRydWUiLCJQbGFuVHlwZSI6IjIwIiwiVXNlUG9saWNpZXMiOiJUcnVlIiwiVXNlU3NvIjoiVHJ1ZSIsIlVzZUtleUNvbm5lY3RvciI6IlRydWUiLCJVc2VTY2ltIjoiVHJ1ZSIsIlVzZUdyb3VwcyI6IlRydWUiLCJVc2VFdmVudHMiOiJUcnVlIiwiVXNlRGlyZWN0b3J5IjoiVHJ1ZSIsIlVzZVRvdHAiOiJUcnVlIiwiVXNlMmZhIjoiVHJ1ZSIsIlVzZUFwaSI6IlRydWUiLCJVc2VSZXNldFBhc3N3b3JkIjoiVHJ1ZSIsIlNlbGZIb3N0IjoiVHJ1ZSIsIlVzZXJzR2V0UHJlbWl1bSI6IlRydWUiLCJVc2VDdXN0b21QZXJtaXNzaW9ucyI6IlRydWUiLCJVc2VQYXNzd29yZE1hbmFnZXIiOiJUcnVlIiwiVXNlU2VjcmV0c01hbmFnZXIiOiJGYWxzZSIsIkxpbWl0Q29sbGVjdGlvbkNyZWF0aW9uRGVsZXRpb24iOiJGYWxzZSIsIkFsbG93QWRtaW5BY2Nlc3NUb0FsbENvbGxlY3Rpb25JdGVtcyI6IkZhbHNlIiwiVXNlUmlza0luc2lnaHRzIjoiRmFsc2UiLCJJc3N1ZWQiOiIwNS8xNC8yMDI2IDE4OjQ1OjU2IiwiRXhwaXJlcyI6IjA1LzE1LzIwMjYgMjA6Mzc6MDMiLCJSZWZyZXNoIjoiMDUvMTUvMjAyNiAyMDozNzowMyIsIlRyaWFsIjoiVHJ1ZSIsIlVzZUFkbWluU3BvbnNvcmVkRmFtaWxpZXMiOiJGYWxzZSIsIlVzZU9yZ2FuaXphdGlvbkRvbWFpbnMiOiJUcnVlIiwiVXNlQXV0b21hdGljVXNlckNvbmZpcm1hdGlvbiI6IkZhbHNlIiwiVXNlRGlzYWJsZVNtQWRzRm9yVXNlcnMiOiJGYWxzZSIsIlVzZVBoaXNoaW5nQmxvY2tlciI6IkZhbHNlIiwiVXNlTXlJdGVtcyI6IlRydWUiLCJVc2VJbnZpdGVMaW5rcyI6IlRydWUiLCJOYW1lIjoidGVzdCIsIkJpbGxpbmdFbWFpbCI6Im0yQGIuY29tIiwiUGxhbiI6IkVudGVycHJpc2UgKEFubnVhbGx5KSIsIkJ1c2luZXNzTmFtZSI6IiIsIkxpY2Vuc2VLZXkiOiJ5ZjZDV3ZvNHRoeTkxTXp5R0pCeCIsIkluc3RhbGxhdGlvbklkIjoiY2ZjNmE3MzQtNGM2Yy00YmE1LWI2MzktYjI4NzAwYWYyZTgxIiwiU2VhdHMiOiIxMCIsIk1heFN0b3JhZ2VHYiI6IjUiLCJqdGkiOiJlZWRlM2QwMS00MmIyLTQ5NzktODUxZS1iNzVlOTI4ZTQxNjciLCJuYmYiOjE3Nzg3ODQzNTYsImV4cCI6MTgxMDMyMDM1NiwiaWF0IjoxNzc4Nzg0MzU2LCJpc3MiOiJiaXR3YXJkZW4iLCJhdWQiOiJvcmdhbml6YXRpb246NWFlNDVlOTgtOWJkNi00NzdlLTk3YjYtYjQ0NDAxNTNjMzBlIn0.gVSbQLUmZ3giHtErkv9LZmbfPdywIdtxypNX8pG55I5F6T6Womz4t7SkdUiFbab5gEbON0AfaoPjEoxVD4m-FAYsqbdODwH_Ftq-PZmFhopk0LUI7Z_AVpFLKFI3vw6hNlMLBG5PPkovcQbKOMZg0Q-JEK9AnF7Q4et-0x03-FzeOPfhbyY6NqEzSdU6CBeto3uqTBXji9GB5hiyPMsJo6ON0CeBYU--k5mj1wl9oFXd_YIsgNuq4AH1BqdBStpu2-lFo1-8qb99eXDCvlCz26rXgxAZVzEBO4CZr5cj4KxkyVgjC0iWAsJu7Adhl5KRxdUEQV8n4ImiMR141bMTzU8NTJBFfJgGPU6_mg5GomNFUcgxtjCN9ldUQ68RLoaT8TaY6FgZxRdRx6qwS431RWWVB16az3jwRvkzzcoCExasgozabunqK-1k3WWgjRyJjf3pXm7mZTDLfFXlSJIJptr0h-U0RqqVnDVTZE6x0Uhs2Qgke094LlSMK8N3OBjaJS-4T4uf2arsvvgm0hpDXgYJwBZyPc62RvO9it9oT9Iaec5UbhHr7CIxJcKRw3ufrZc2pw4RDL2Y7rD0PdMZL6RfoGCzFP-z17OdDSk_aLNloE6dY6ivFrJhukULbvjd16lBl_Y2856LxTThpSq1CFty-M-rcaNImbRxh04TF-A" +} \ No newline at end of file diff --git a/src/Api/c:/bwtest/licenses/user/511b2fe6-4a9f-4a32-8598-b43d0117cc3a.json b/src/Api/c:/bwtest/licenses/user/511b2fe6-4a9f-4a32-8598-b43d0117cc3a.json new file mode 100644 index 000000000000..78b75fd39fd5 --- /dev/null +++ b/src/Api/c:/bwtest/licenses/user/511b2fe6-4a9f-4a32-8598-b43d0117cc3a.json @@ -0,0 +1,17 @@ +{ + "LicenseKey": "PKExN0Be2fN1YiYOzfoW", + "Id": "24a35553-d105-43b1-bd2f-b44500b63250", + "Name": null, + "Email": "legacy-attach@bit.local", + "Premium": true, + "MaxStorageGb": 5, + "Version": 1, + "Issued": "2026-05-09T11:06:50.478911Z", + "Refresh": "2027-05-09T11:03:46Z", + "Expires": "2027-05-16T11:03:46", + "Trial": false, + "LicenseType": 0, + "Hash": "e4wf2yinis6xzzRcfqmbyR\u002BVdl1f\u002BtgPBTACbtpiUmY=", + "Signature": "XscYrmc/ExR7m84cUooHheAGRiL3SOjWM8IOmUYMSKeIYmsiJExZiys9lUa4pg3bJ8R\u002BIWCC25H\u002BxR2hDxrDWCSN8r326hZgNCDX5V8SGoN0a1HlZ7HTN\u002BFn3ZUPNdy93yqgrnEQcYt\u002BTzpNiNmId1D0mpBAea1SdzZNHmS4Ep0AuUP0kCpPNMamZ1wlW5CIEm2o\u002BYw9n9lRNwLkiQl5AHHKm8Ni5IbIA\u002BbDJ4mUpaD2Wt/AmGpk3d5ArvymCkZnYYHoGqmdvFlsltoocgLgvWV44qYEpvYNBj2ydGzkA5YTElg5hvG1\u002BBxI4zNc9DILf7kuKU5\u002BloCc1Qay7Qca\u002BD4c1PjMEEcM7VbwSmPp1Vah82FiDPVT627i1egriJM0XuSsdzxnxcEq8QKgOlvYje5BIdYaMlOW/1kk0pWHp6pUzlPVM0Wob6kzpxHQiXIre6y48MYOXMrNan8DO4kaKSwR3PRtOMZ5HjAvoBYDz\u002BSQJOJPv6XvitEzt3i8Hq50FRBa7L9PRj7FBPpmDMJCPMMPahc95pkYDUItbnKKz05bUY5YCHLIL9xmupFQz/Z9XP8ZqcSUkQJWrP4Hjlvje00xoOXa0CD4vk5F916fBaTr3l/RB/nOTuBMkghH8JoVtFBaqCC8b8Yk/Mu6zXW0GBLydWkFl1qC8vFbDVoZYBA=", + "Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJMaWNlbnNlVHlwZSI6IlVzZXIiLCJJZCI6IjI0YTM1NTUzLWQxMDUtNDNiMS1iZDJmLWI0NDUwMGI2MzI1MCIsIlByZW1pdW0iOiJUcnVlIiwiSXNzdWVkIjoiMDUvMDkvMjAyNiAxMTowNjo1MCIsIlRyaWFsIjoiRmFsc2UiLCJFbWFpbCI6ImxlZ2FjeS1hdHRhY2hAYml0LmxvY2FsIiwiTGljZW5zZUtleSI6IlBLRXhOMEJlMmZOMVlpWU96Zm9XIiwiTWF4U3RvcmFnZUdiIjoiNSIsIkV4cGlyZXMiOiIwNS8xNi8yMDI3IDExOjAzOjQ2IiwiUmVmcmVzaCI6IjA1LzA5LzIwMjcgMTE6MDM6NDYiLCJqdGkiOiIzOTNlZDgzNi02MjU3LTQyOWEtYjczMi0wOWJjYTM1YzY4MDMiLCJuYmYiOjE3NzgzMjQ4MTAsImV4cCI6MTgwOTg2MDgxMCwiaWF0IjoxNzc4MzI0ODEwLCJpc3MiOiJiaXR3YXJkZW4iLCJhdWQiOiJ1c2VyOjI0YTM1NTUzLWQxMDUtNDNiMS1iZDJmLWI0NDUwMGI2MzI1MCJ9.Ibsg6oSOiVtiTa9YnQwHQsV_HpuZ5XvrN1DOdRD_UDhF353QH82NJ_xud_4bPt3S0X9JC8BvyUatYOQhsSyXTMCkvpmHum-t3UotERoWutIhD8E5RgrUeYUo8S-C9zZZ9VyZ95jWbTPUfSyQGpHtbZHKzIY3KucTrjdNu2llqxROLLIUTMFoz36LNjzzIohf-eMfW_hJU7rVotuJKZpG4GqdTtqYmvhCWmQgbCirZukBLYX_Z2fGEwRu7B9KRV2AIwh70FuhTDV9V1SBtmhjOlgt6eCSW9GSusvOXYjw5bCBqjA168qe0uyRrEcqpF-lLex5kv8-r2fTwxeAQZzBrQUJwj0aQEjhyPAZEMAT4CSrez8tHp2k1VzWHn_jkeXvZKaaOz-EojwWsxcFDuzFQ7AGbmy5-zxZbpA1J0MVbfBO1IV02uVBmsUFLrp_rt3THyZga5c6ckP1utBRJFVDXDr7wnEzBWQkDRweAs5mkxWe73VZKTLlwR6C0vsRaHmvD-pXDHcsR7pYBqYiJNU2LaA0QfL92ULNSzjhWjGsk0gBlsn6u5zcJsd2t2BTwP_-vPwgzh3YxB1V7g5o4s6AvytWgoEHpke3upDkLM_Uq7TgFrNnkrnjlNDWHMul__68S2ZJeC2qouURvqaggkIzG_PhnwyfAe3VVIn5HJ0yAqM" +} \ No newline at end of file diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 5b021831097a..52f55c06b905 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -88,7 +87,7 @@ Task> GetManyDetailsByUserAsync /// /// The user that initiated the key rotation /// A list of organization users with updated reset password keys - UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, + DatabaseTransactionAction UpdateForKeyRotation(Guid userId, IEnumerable resetPasswordKeys); /// diff --git a/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs b/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs index e46a0520e5fe..02dafc8ecf9c 100644 --- a/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs +++ b/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; -using Bit.Core.KeyManagement.UserKey; namespace Bit.Core.Repositories; @@ -40,7 +39,7 @@ public interface IEmergencyAccessRepository : IRepository /// /// The grantor that initiated the key rotation /// A list of emergency access with updated keys - UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, + DatabaseTransactionAction UpdateForKeyRotation(Guid grantorId, IEnumerable emergencyAccessKeys); /// diff --git a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs index 29ed9d2210bf..859a6f8ed6b1 100644 --- a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs +++ b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; #nullable enable @@ -12,5 +11,5 @@ public interface IWebAuthnCredentialRepository : IRepository GetByIdAsync(Guid id, Guid userId); Task> GetManyByUserIdAsync(Guid userId); Task UpdateAsync(WebAuthnCredential credential); - UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials); + DatabaseTransactionAction UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials); } diff --git a/src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs b/src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs index ce8979620f8e..bfb20332e24b 100644 --- a/src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs +++ b/src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs @@ -1,7 +1,6 @@  using Bit.Core.KeyManagement.Entities; using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; namespace Bit.Core.KeyManagement.Repositories; @@ -9,6 +8,6 @@ namespace Bit.Core.KeyManagement.Repositories; public interface IUserSignatureKeyPairRepository : IRepository { public Task GetByUserIdAsync(Guid userId); - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signatureKeyPair); - public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signatureKeyPair); + public DatabaseTransactionAction UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signatureKeyPair); + public DatabaseTransactionAction SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signatureKeyPair); } diff --git a/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs index 14c9974f0083..3b80076e6dc4 100644 --- a/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs @@ -5,7 +5,6 @@ using Bit.Core.Exceptions; using Bit.Core.KeyManagement.UserKey.Models.Data; using Microsoft.AspNetCore.Identity; -using Microsoft.Data.SqlClient; namespace Bit.Core.KeyManagement.UserKey; @@ -51,12 +50,3 @@ public interface IRotateUserAccountKeysCommand /// Thrown when is not a key connector user. Task KeyConnectorRotateUserAccountKeysAsync(User user, KeyConnectorRotateUserAccountKeysData model); } - -/// -/// A type used to implement updates to the database for key rotations. Each domain that requires an update of encrypted -/// data during a key rotation should use this to implement its own database call. The user repository loops through -/// these during a key rotation. -/// Note: connection and transaction are only used for Dapper. They won't be available in EF -/// -public delegate Task UpdateEncryptedDataForKeyRotation(SqlConnection connection = null, - SqlTransaction transaction = null); diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs index f7ebf2701dde..4a5cfaba3a5c 100644 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs @@ -89,7 +89,7 @@ public async Task PasswordChangeAndRotateUserAccountKeysAsync(Us model.ValidateForUser(user); - List saveEncryptedDataActions = []; + List saveEncryptedDataActions = []; // A manual key rotation always logs the user out, so a V2 upgrade token is never needed here. // Discard anything the client submitted, which also clears a token left over from an earlier upgrade. @@ -128,7 +128,7 @@ public async Task MasterPasswordRotateUserAccountKeysAsync(User user, MasterPass model.ValidateForUser(user); - List saveEncryptedDataActions = []; + List saveEncryptedDataActions = []; var shouldPersistV2UpgradeToken = await BaseRotateUserAccountKeysAsync(model.BaseData, user, saveEncryptedDataActions); user.Key = model.MasterPasswordUnlockData.MasterKeyWrappedUserKey; @@ -145,7 +145,7 @@ public async Task TdeRotateUserAccountKeysAsync(User user, TdeRotateUserAccountK model.ValidateForUser(user); - List saveEncryptedDataActions = []; + List saveEncryptedDataActions = []; var shouldPersistV2UpgradeToken = await BaseRotateUserAccountKeysAsync(model.BaseData, user, saveEncryptedDataActions); await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions); @@ -160,7 +160,7 @@ public async Task KeyConnectorRotateUserAccountKeysAsync(User user, KeyConnector model.ValidateForUser(user); - List saveEncryptedDataActions = []; + List saveEncryptedDataActions = []; var shouldPersistV2UpgradeToken = await BaseRotateUserAccountKeysAsync(model.BaseData, user, saveEncryptedDataActions); user.Key = model.KeyConnectorKeyWrappedUserKey; @@ -169,7 +169,7 @@ public async Task KeyConnectorRotateUserAccountKeysAsync(User user, KeyConnector await HandlePushNotificationAsync(shouldPersistV2UpgradeToken, user); } - private async Task RotateV2AccountKeysAsync(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + private async Task RotateV2AccountKeysAsync(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) { ValidateV2Encryption(model); await ValidateVerifyingKeyUnchangedAsync(model, user); @@ -180,7 +180,7 @@ private async Task RotateV2AccountKeysAsync(BaseRotateUserAccountKeysData model, user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion; } - private void UpgradeV1ToV2Keys(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + private void UpgradeV1ToV2Keys(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) { ValidateV2Encryption(model); saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.SetUserSignatureKeyPair(user.Id, model.AccountKeys.SignatureKeyPairData)); @@ -189,7 +189,7 @@ private void UpgradeV1ToV2Keys(BaseRotateUserAccountKeysData model, User user, L user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion; } - internal async Task UpdateAccountKeysAsync(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + internal async Task UpdateAccountKeysAsync(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) { ValidatePublicKeyEncryptionKeyPairUnchanged(model, user); @@ -214,7 +214,7 @@ internal async Task UpdateAccountKeysAsync(BaseRotateUserAccountKeysData model, user.PrivateKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey; } - internal void UpdateUserData(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + internal void UpdateUserData(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) { // The revision date has to be updated so that de-synced clients don't accidentally post over the re-encrypted data // with an old-user key-encrypted copy @@ -294,7 +294,7 @@ private static void ValidateV2Encryption(BaseRotateUserAccountKeysData model) } } - private void UpdateBaseUnlockMethods(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + private void UpdateBaseUnlockMethods(BaseRotateUserAccountKeysData model, User user, List saveEncryptedDataActions) { if (model.EmergencyAccesses.Any()) { @@ -318,7 +318,7 @@ private void UpdateBaseUnlockMethods(BaseRotateUserAccountKeysData model, User u } private async Task BaseRotateUserAccountKeysAsync(BaseRotateUserAccountKeysData baseModel, User user, - List saveEncryptedDataActions) + List saveEncryptedDataActions) { var now = DateTime.UtcNow; user.RevisionDate = user.AccountRevisionDate = now; diff --git a/src/Core/Repositories/DatabaseTransactionAction.cs b/src/Core/Repositories/DatabaseTransactionAction.cs new file mode 100644 index 000000000000..ae4aa92a19f4 --- /dev/null +++ b/src/Core/Repositories/DatabaseTransactionAction.cs @@ -0,0 +1,9 @@ +using System.Data.Common; + +namespace Bit.Core.Repositories; + +/// +/// A database operation that participates in an existing database connection and transaction. +/// Used to compose multiple repository operations into a single atomic transaction. +/// +public delegate Task DatabaseTransactionAction(DbConnection connection, DbTransaction transaction); diff --git a/src/Core/Repositories/IDeviceRepository.cs b/src/Core/Repositories/IDeviceRepository.cs index d4ce7a299a72..235b0438ecf2 100644 --- a/src/Core/Repositories/IDeviceRepository.cs +++ b/src/Core/Repositories/IDeviceRepository.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; -using Bit.Core.KeyManagement.UserKey; #nullable enable @@ -17,7 +16,7 @@ public interface IDeviceRepository : IRepository // other requests. Task> GetManyByUserIdWithDeviceAuth(Guid userId); Task ClearPushTokenAsync(Guid id); - UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable devices); + DatabaseTransactionAction UpdateKeysForRotationAsync(Guid userId, IEnumerable devices); /// /// Updates the device's last-activity state: moves LastActivityDate to today (if not /// already today) and writes to ClientVersion (if diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index c7f5bc1668f0..92e3a5748b24 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -2,7 +2,6 @@ using Bit.Core.Billing.Premium.Models; using Bit.Core.Entities; using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; namespace Bit.Core.Repositories; @@ -60,9 +59,9 @@ public interface IUserRepository : IRepository /// The user to update /// Registered database calls to update re-encrypted data. Task UpdateUserKeyAndEncryptedDataAsync(User user, - IEnumerable updateDataActions); + IEnumerable updateDataActions); Task UpdateUserKeyAndEncryptedDataV2Async(User user, - IEnumerable updateDataActions); + IEnumerable updateDataActions); /// /// Sets the account cryptographic state to a user in a single transaction. The provided /// MUST be a V2 encryption state. Passing in a V1 encryption state will throw. diff --git a/src/Core/Tools/Repositories/ISendRepository.cs b/src/Core/Tools/Repositories/ISendRepository.cs index 4f7ced15df5e..55c40bda9d6a 100644 --- a/src/Core/Tools/Repositories/ISendRepository.cs +++ b/src/Core/Tools/Repositories/ISendRepository.cs @@ -1,6 +1,5 @@ #nullable enable -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Tools.Entities; @@ -76,7 +75,7 @@ public interface ISendRepository : IRepository /// /// The user that initiated the key rotation /// A list of sends with updated data - UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, + DatabaseTransactionAction UpdateForKeyRotation(Guid userId, IEnumerable sends); /// diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index bc4bd64cfa7f..4c15e97a387c 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -1,5 +1,4 @@ using Bit.Core.Entities; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; @@ -68,7 +67,7 @@ Task> GetCipherPermissionsForOrganizat /// /// The user that initiated the key rotation /// A list of ciphers with updated data - UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, + DatabaseTransactionAction UpdateForKeyRotation(Guid userId, IEnumerable ciphers); /// diff --git a/src/Core/Vault/Repositories/IFolderRepository.cs b/src/Core/Vault/Repositories/IFolderRepository.cs index c4693b2a1343..9073706e5c74 100644 --- a/src/Core/Vault/Repositories/IFolderRepository.cs +++ b/src/Core/Vault/Repositories/IFolderRepository.cs @@ -1,5 +1,4 @@ -using Bit.Core.KeyManagement.UserKey; -using Bit.Core.Repositories; +using Bit.Core.Repositories; using Bit.Core.Vault.Entities; namespace Bit.Core.Vault.Repositories; @@ -14,6 +13,6 @@ public interface IFolderRepository : IRepository /// /// The user that initiated the key rotation /// A list of folders with updated data - UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, + DatabaseTransactionAction UpdateForKeyRotation(Guid userId, IEnumerable folders); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 418adf51325e..2dbf2644093c 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Utilities.DebuggingInstruments; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; @@ -596,7 +595,7 @@ public async Task> GetManyAcco } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid userId, IEnumerable resetPasswordKeys) { return async (connection, transaction) => diff --git a/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs index c76dfb525d17..39da66a1f2d3 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs @@ -1,7 +1,6 @@ using System.Data; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Auth.Helpers; @@ -124,11 +123,14 @@ public async Task> GetExpiredRecoveriesAsync } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid grantorId, IEnumerable emergencyAccessKeys) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (dbConnection, dbTransaction) => { + var connection = (SqlConnection)dbConnection; + var transaction = (SqlTransaction)dbTransaction; + // Create temp table var sqlCreateTemp = @" SELECT TOP 0 * diff --git a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs index 7dfcd15d4921..3c6290238921 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -2,7 +2,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.Repositories; @@ -61,9 +61,9 @@ public async Task UpdateAsync(WebAuthnCredential credential) return affectedRows > 0; } - public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials) + public DatabaseTransactionAction UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (connection, transaction) => { const string sql = @" UPDATE WC diff --git a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs index 5dcc2943b8f7..9f501d9092aa 100644 --- a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs +++ b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs @@ -2,7 +2,7 @@ using Bit.Core.KeyManagement.Entities; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Repositories; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.Repositories; @@ -37,9 +37,9 @@ public UserSignatureKeyPairRepository(string connectionString, string readOnlyCo } } - public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys) + public DatabaseTransactionAction SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (connection, transaction) => { await connection.QueryAsync( "[dbo].[UserSignatureKeyPair_SetForRotation]", @@ -58,9 +58,9 @@ await connection.QueryAsync( }; } - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys) + public DatabaseTransactionAction UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (connection, transaction) => { await connection.QueryAsync( "[dbo].[UserSignatureKeyPair_UpdateForRotation]", diff --git a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs index f165d60955f5..8198a9374453 100644 --- a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs @@ -1,7 +1,6 @@ using System.Data; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -130,9 +129,9 @@ await connection.ExecuteAsync( } } - public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) + public DatabaseTransactionAction UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (connection, transaction) => { const string sql = @" UPDATE D diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index 0ea07d4bd40c..04cdb7e21c9b 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -6,7 +6,6 @@ using Bit.Core.Enums; using Bit.Core.KeyManagement.Kdf; using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -242,7 +241,7 @@ await connection.ExecuteAsync( /// public async Task UpdateUserKeyAndEncryptedDataAsync( User user, - IEnumerable updateDataActions) + IEnumerable updateDataActions) { await using var connection = new SqlConnection(ConnectionString); connection.Open(); @@ -292,7 +291,7 @@ public async Task UpdateUserKeyAndEncryptedDataAsync( public async Task UpdateUserKeyAndEncryptedDataV2Async( User user, - IEnumerable updateDataActions) + IEnumerable updateDataActions) { await using var connection = new SqlConnection(ConnectionString); connection.Open(); diff --git a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs index a62d649c9dfc..e123e918f2b7 100644 --- a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs +++ b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs @@ -3,7 +3,7 @@ using System.Data; using System.Security.Cryptography; using Bit.Core; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Repositories; @@ -132,10 +132,13 @@ public override async Task ReplaceAsync(Send send) } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, IEnumerable sends) + public DatabaseTransactionAction UpdateForKeyRotation(Guid userId, IEnumerable sends) { - return async (connection, transaction) => + return async (dbConnection, dbTransaction) => { + var connection = (SqlConnection)dbConnection; + var transaction = (SqlTransaction)dbTransaction; + // Protect all sends before bulk update var sendsList = sends.ToList(); foreach (var send in sendsList) diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index fb43858902f4..aa58e365d244 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -4,7 +4,7 @@ using System.Data; using System.Text.Json; using Bit.Core.Entities; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Utilities; @@ -366,11 +366,14 @@ public async Task> GetUserSecurityTasksByCip } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid userId, IEnumerable ciphers) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (dbConnection, dbTransaction) => { + var connection = (SqlConnection)dbConnection; + var transaction = (SqlTransaction)dbTransaction; + // Create temp table var sqlCreateTemp = @" SELECT TOP 0 * diff --git a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs index 63da064f8808..c6313de347a0 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs @@ -2,7 +2,7 @@ #nullable disable using System.Data; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; @@ -48,11 +48,14 @@ public async Task> GetManyByUserIdAsync(Guid userId) } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid userId, IEnumerable folders) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (dbConnection, dbTransaction) => { + var connection = (SqlConnection)dbConnection; + var transaction = (SqlTransaction)dbTransaction; + // Create temp table var sqlCreateTemp = @" SELECT TOP 0 * diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index d98d59836e7f..d2353edd2370 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -8,7 +8,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; @@ -857,14 +856,14 @@ on ou.OrganizationId equals o.Id } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid userId, IEnumerable resetPasswordKeys) { - return async (_, _) => + return async (connection, transaction) => { var newOrganizationUsers = resetPasswordKeys.ToList(); using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); // Get user organization users var userOrganizationUsers = await GetDbSet(dbContext) diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs index ed811e9e0983..e13b2a4e6a8d 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs @@ -1,12 +1,10 @@ using AutoMapper; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; -using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -153,14 +151,14 @@ public async Task> GetManyToNotifyAsync() } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid grantorId, IEnumerable emergencyAccessKeys) { - return async (SqlConnection connection, SqlTransaction transaction) => + return async (connection, transaction) => { var newKeys = emergencyAccessKeys.ToList(); using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var userEmergencyAccess = await GetDbSet(dbContext) .Where(ea => ea.GrantorId == grantorId) .ToListAsync(); diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs index ca32c44211bf..182f316a5c34 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -1,7 +1,7 @@ using AutoMapper; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; @@ -61,13 +61,13 @@ public async Task UpdateAsync(Core.Auth.Entities.WebAuthnCredential creden } } - public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials) + public DatabaseTransactionAction UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials) { - return async (_, _) => + return async (connection, transaction) => { var newCreds = credentials.ToList(); using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var newCredIds = newCreds.Select(nwc => nwc.Id).ToList(); var validUserWebauthnCredentials = await GetDbSet(dbContext) diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs index 04f055501d09..11c645c0c9a0 100644 --- a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs +++ b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs @@ -2,7 +2,7 @@ using AutoMapper; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Repositories; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; @@ -25,12 +25,12 @@ public class UserSignatureKeyPairRepository(IServiceScopeFactory serviceScopeFac return signingKeys.ToSignatureKeyPairData(); } - public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys) + public DatabaseTransactionAction SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys) { - return async (_, _) => + return async (connection, transaction) => { await using var scope = ServiceScopeFactory.CreateAsyncScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var entity = new Models.UserSignatureKeyPair { Id = CoreHelpers.GenerateComb(), @@ -46,12 +46,12 @@ public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, Si }; } - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys) + public DatabaseTransactionAction UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys) { - return async (_, _) => + return async (connection, transaction) => { await using var scope = ServiceScopeFactory.CreateAsyncScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var entity = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == grantorId); if (entity != null) { diff --git a/src/Infrastructure.EntityFramework/Repositories/BaseEntityFrameworkRepository.cs b/src/Infrastructure.EntityFramework/Repositories/BaseEntityFrameworkRepository.cs index 6cf7cbb46efc..c98c8ff085f1 100644 --- a/src/Infrastructure.EntityFramework/Repositories/BaseEntityFrameworkRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/BaseEntityFrameworkRepository.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Data.Common; +using System.Text.Json; using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Repositories.Queries; @@ -31,6 +32,15 @@ public DatabaseContext GetDatabaseContext(IServiceScope serviceScope) return serviceScope.ServiceProvider.GetRequiredService(); } + protected DatabaseContext GetTransactionalDatabaseContext( + IServiceScope scope, DbConnection connection, DbTransaction transaction) + { + var dbContext = GetDatabaseContext(scope); + dbContext.Database.SetDbConnection(connection); + dbContext.Database.UseTransaction(transaction); + return dbContext; + } + public void ClearChangeTracking() { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs b/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs index cbc7709d204c..4e2c70fe5c53 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs @@ -1,6 +1,5 @@ using AutoMapper; using Bit.Core.Auth.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries; @@ -200,13 +199,13 @@ await dbContext.Devices : d.ClientVersion)); } - public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) + public DatabaseTransactionAction UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) { - return async (_, _) => + return async (connection, transaction) => { var deviceUpdates = devices.ToList(); using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var userDevices = await GetDbSet(dbContext) .Where(device => device.UserId == userId) .ToListAsync(); diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index d7b10aa62b2f..790148757dea 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -4,7 +4,6 @@ using Bit.Core.Enums; using Bit.Core.KeyManagement.Kdf; using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; @@ -195,12 +194,15 @@ public async Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalRemind /// public async Task UpdateUserKeyAndEncryptedDataAsync(Core.Entities.User user, - IEnumerable updateDataActions) + IEnumerable updateDataActions) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); - await using var transaction = await dbContext.Database.BeginTransactionAsync(); + var connection = dbContext.Database.GetDbConnection(); + await connection.OpenAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + await dbContext.Database.UseTransactionAsync(transaction); try { @@ -225,8 +227,7 @@ public async Task UpdateUserKeyAndEncryptedDataAsync(Core.Entities.User user, // Update re-encrypted data foreach (var action in updateDataActions) { - // connection and transaction aren't used in EF - await action(); + await action(connection, transaction); } await transaction.CommitAsync(); @@ -241,12 +242,15 @@ public async Task UpdateUserKeyAndEncryptedDataAsync(Core.Entities.User user, public async Task UpdateUserKeyAndEncryptedDataV2Async(Core.Entities.User user, - IEnumerable updateDataActions) + IEnumerable updateDataActions) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); - await using var transaction = await dbContext.Database.BeginTransactionAsync(); + var connection = dbContext.Database.GetDbConnection(); + await connection.OpenAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + await dbContext.Database.UseTransactionAsync(transaction); // Update user var userEntity = await dbContext.Users.FindAsync(user.Id); @@ -287,8 +291,7 @@ public async Task UpdateUserKeyAndEncryptedDataV2Async(Core.Entities.User user, // Update re-encrypted data foreach (var action in updateDataActions) { - // connection and transaction aren't used in EF - await action(); + await action(connection, transaction); } await transaction.CommitAsync(); diff --git a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs index 0ab18e3d255d..13882c7f8937 100644 --- a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs +++ b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs @@ -3,7 +3,7 @@ using System.Security.Cryptography; using AutoMapper; using Bit.Core; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Repositories; using Bit.Infrastructure.EntityFramework.Models; @@ -150,17 +150,17 @@ public override async Task ReplaceAsync(Core.Tools.Entities.Send send) } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, + public DatabaseTransactionAction UpdateForKeyRotation(Guid userId, IEnumerable sends) { - return async (_, _) => + return async (connection, transaction) => { // No Emails protect/unprotect needed here: this only mutates Key on tracked entities, and EF // writes only the changed column, so the already-protected Emails at rest is untouched. (The // Dapper implementation protects because it bulk-copies whole rows.) var newSends = sends.ToDictionary(s => s.Id); using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var userSends = await GetDbSet(dbContext) .Where(s => s.UserId == userId) .ToListAsync(); diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index e74c05b2293e..dcc119664e7d 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -5,7 +5,7 @@ using System.Text.Json.Nodes; using AutoMapper; using Bit.Core.Enums; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; @@ -19,7 +19,6 @@ using Bit.Infrastructure.EntityFramework.Vault.Models; using Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries; using LinqToDB.EntityFrameworkCore; -using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using NS = Newtonsoft.Json; @@ -1035,14 +1034,14 @@ public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid userId, IEnumerable ciphers) { - return async (SqlConnection _, SqlTransaction _) => + return async (connection, transaction) => { var newCiphers = ciphers.ToList(); using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var userCiphers = await GetDbSet(dbContext) .Where(c => c.UserId == userId) .ToListAsync(); diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs index 83fa442eb4f9..406a4e81c4d1 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs @@ -2,11 +2,10 @@ #nullable disable using AutoMapper; -using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Models; -using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -43,14 +42,14 @@ public FolderRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper } /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + public DatabaseTransactionAction UpdateForKeyRotation( Guid userId, IEnumerable folders) { - return async (SqlConnection _, SqlTransaction _) => + return async (connection, transaction) => { var newFolders = folders.ToList(); using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); + var dbContext = GetTransactionalDatabaseContext(scope, connection, transaction); var userFolders = await GetDbSet(dbContext) .Where(f => f.UserId == userId) .ToListAsync(); diff --git a/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs b/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs index 3b9218aec24b..f18fcd2ed067 100644 --- a/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs +++ b/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs @@ -6,7 +6,6 @@ using Bit.Core.KeyManagement.Enums; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Repositories; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.KeyManagement.UserKey.Implementations; using Bit.Core.KeyManagement.UserKey.Models.Data; using Bit.Core.Platform.Push; @@ -164,7 +163,7 @@ public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider(); + var saveEncryptedDataActions = new List(); await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); } @@ -176,7 +175,7 @@ public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(S SetV2ModelUser(model); model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); } @@ -188,7 +187,7 @@ public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects( SetV1ModelUser(model); model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType7String; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("The provided account private key was not wrapped with AES-256-CBC-HMAC", ex.Message); } @@ -200,7 +199,7 @@ public async Task UpdateAccountKeysAsync_V1_Success(SutProvider(); + var saveEncryptedDataActions = new List(); await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions); Assert.Empty(saveEncryptedDataActions); } @@ -212,7 +211,7 @@ public async Task UpdateAccountKeysAsync_V2_Success(SutProvider(); + var saveEncryptedDataActions = new List(); await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions); Assert.NotEmpty(saveEncryptedDataActions); Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState); @@ -226,7 +225,7 @@ public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(Sut SetV2ModelUser(model); model.AccountKeys.SignatureKeyPairData.VerifyingKey = "different-verifying-key"; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("The provided verifying key does not match the user's current verifying key.", ex.Message); } @@ -239,7 +238,7 @@ public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejec SetV2ModelUser(model); model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message); } @@ -252,7 +251,7 @@ public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Re SetV2ModelUser(model); model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = _mockEncryptedType2String; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("The provided signing key data is not wrapped with XChaCha20-Poly1305.", ex.Message); } @@ -265,7 +264,7 @@ public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutP SetV2ModelUser(model); model.AccountKeys.SignatureKeyPairData.VerifyingKey = ""; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("The provided signature key pair data does not contain a valid verifying key.", ex.Message); } @@ -278,7 +277,7 @@ public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKe SetV2ModelUser(model); model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.", ex.Message); } @@ -291,7 +290,7 @@ public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(S SetV2ModelUser(model); model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message); } @@ -304,7 +303,7 @@ public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(Sut SetV2ModelUser(model); model.AccountKeys.SecurityStateData = null; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("No signed security state provider for V2 user", ex.Message); } @@ -317,7 +316,7 @@ public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(Sut SetV2ModelUser(model); model.AccountKeys.SignatureKeyPairData = null; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("Signature key pair data is required for V2 encryption.", ex.Message); } @@ -330,7 +329,7 @@ public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(S SetV1ModelUser(model); model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = ""; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("Invalid encryption type string.", ex.Message); } @@ -343,7 +342,7 @@ public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects SetV1ModelUser(model); model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "9.xxx"; - var saveEncryptedDataActions = new List(); + var saveEncryptedDataActions = new List(); var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); Assert.Equal("Invalid encryption type string.", ex.Message); } @@ -365,12 +364,12 @@ public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider(); + var saveEncryptedDataActions = new List(); sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions); foreach (var dataAction in saveEncryptedDataActions) { - await dataAction.Invoke(); + await dataAction.Invoke(null!, null!); } var updatedCiphers = sutProvider.GetDependency() @@ -644,7 +643,7 @@ public async Task PasswordChangeAndRotateUserAccountKeysAsync_MasterPasswordServ Assert.False(result.Succeeded); Assert.Contains(result.Errors, e => e.Code == "SomeError"); await sutProvider.GetDependency().DidNotReceive() - .UpdateUserKeyAndEncryptedDataV2Async(Arg.Any(), Arg.Any>()); + .UpdateUserKeyAndEncryptedDataV2Async(Arg.Any(), Arg.Any>()); await sutProvider.GetDependency().DidNotReceive() .PushLogOutAsync(Arg.Any(), Arg.Any(), Arg.Any()); } @@ -739,7 +738,7 @@ public async Task MasterPasswordRotateUserAccountKeysAsync_V2User_Success( Assert.Equal(model.MasterPasswordUnlockData.MasterKeyWrappedUserKey, user.Key); await sutProvider.GetDependency().Received(1) - .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); + .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); await sutProvider.GetDependency().Received(1) .PushLogOutAsync(user.Id); @@ -937,7 +936,7 @@ public async Task TdeRotateUserAccountKeysAsync_V1User_Success( Assert.Null(user.Key); Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); await sutProvider.GetDependency().Received(1) - .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); + .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); await sutProvider.GetDependency().Received(1) .PushLogOutAsync(user.Id); } @@ -958,7 +957,7 @@ public async Task TdeRotateUserAccountKeysAsync_V2User_Success( Assert.Null(user.Key); Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); await sutProvider.GetDependency().Received(1) - .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); + .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); await sutProvider.GetDependency().Received(1) .PushLogOutAsync(user.Id); } @@ -1079,7 +1078,7 @@ public async Task KeyConnectorRotateUserAccountKeysAsync_V1User_Success( Assert.Equal(model.KeyConnectorKeyWrappedUserKey, user.Key); Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); await sutProvider.GetDependency().Received(1) - .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); + .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); await sutProvider.GetDependency().Received(1) .PushLogOutAsync(user.Id); } @@ -1100,7 +1099,7 @@ public async Task KeyConnectorRotateUserAccountKeysAsync_V2User_Success( Assert.Equal(model.KeyConnectorKeyWrappedUserKey, user.Key); Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); await sutProvider.GetDependency().Received(1) - .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); + .UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any>()); await sutProvider.GetDependency().Received(1) .PushLogOutAsync(user.Id); } diff --git a/test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs b/test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs index dabddc605851..086e82fc5ebc 100644 --- a/test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs +++ b/test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; @@ -192,7 +191,7 @@ private class StubDeviceRepository : IDeviceRepository public Task> GetManyByUserIdAsync(Guid userId) => throw new NotImplementedException(); public Task> GetManyByUserIdWithDeviceAuth(Guid userId) => throw new NotImplementedException(); public Task ReplaceAsync(Device obj) => throw new NotImplementedException(); - public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) => throw new NotImplementedException(); + public DatabaseTransactionAction UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) => throw new NotImplementedException(); public Task UpsertAsync(Device obj) => throw new NotImplementedException(); public Task UpdateLastActivityByIdAsync(Guid deviceId, string? clientVersion) => throw new NotImplementedException(); public Task UpdateLastActivityByIdentifierAndUserIdAsync(string identifier, Guid userId, string? clientVersion) => throw new NotImplementedException(); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index c1036369a18f..5b65560091ea 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -17,6 +17,10 @@ namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.Organizat public class OrganizationUserRepositoryTests { + private const string _resetPasswordKey = "4.reset-password-key"; + private const string _v2UpgradeToken = """{"WrappedUserKey1":"7.key-one","WrappedUserKey2":"2.key-two"}"""; + private const string _otherV2UpgradeToken = """{"WrappedUserKey1":"7.key-three","WrappedUserKey2":"2.key-four"}"""; + [Theory, DatabaseData] public async Task GetOccupiedSmSeatCountByOrganizationIdAsync_ExcludesRevokedAndStaged( IUserRepository userRepository, @@ -1793,4 +1797,88 @@ public async Task UpdateGroupsAsync_BumpsGroupRevisionDate( Assert.Equal(expectedRevisionDate, actualGroup1.RevisionDate, TimeSpan.FromMilliseconds(10)); Assert.Equal(expectedRevisionDate, actualGroup2.RevisionDate, TimeSpan.FromMilliseconds(10)); } + + [Theory, DatabaseData] + public async Task UpdateForKeyRotation_WithV2UpgradeToken_PersistsResetPasswordKeyAndToken( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var organizationUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + organizationUser.ResetPasswordKey = _resetPasswordKey; + organizationUser.V2UpgradeToken = _v2UpgradeToken; + + // Act + await userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, + [organizationUserRepository.UpdateForKeyRotation(user.Id, [organizationUser])]); + + // Assert + var updated = await organizationUserRepository.GetByIdAsync(organizationUser.Id); + Assert.NotNull(updated); + Assert.Equal(_resetPasswordKey, updated.ResetPasswordKey); + Assert.Equal(_v2UpgradeToken, updated.V2UpgradeToken); + } + + [Theory, DatabaseData] + public async Task UpdateForKeyRotation_WithoutV2UpgradeToken_ClearsStaleToken( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var organizationUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + // A previous upgrade rotation left a token behind + organizationUser.ResetPasswordKey = _resetPasswordKey; + organizationUser.V2UpgradeToken = _v2UpgradeToken; + await userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, + [organizationUserRepository.UpdateForKeyRotation(user.Id, [organizationUser])]); + + organizationUser.V2UpgradeToken = null; + + // Act + await userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, + [organizationUserRepository.UpdateForKeyRotation(user.Id, [organizationUser])]); + + // Assert + var updated = await organizationUserRepository.GetByIdAsync(organizationUser.Id); + Assert.NotNull(updated); + Assert.Equal(_resetPasswordKey, updated.ResetPasswordKey); + Assert.Null(updated.V2UpgradeToken); + } + + [Theory, DatabaseData] + public async Task UpdateForKeyRotation_WithOtherUsersMembership_LeavesItUnchanged( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync("rotating"); + var otherUser = await userRepository.CreateTestUserAsync("other"); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var otherOrganizationUser = + await organizationUserRepository.CreateTestOrganizationUserAsync(organization, otherUser); + + otherOrganizationUser.ResetPasswordKey = _resetPasswordKey; + otherOrganizationUser.V2UpgradeToken = _v2UpgradeToken; + await userRepository.UpdateUserKeyAndEncryptedDataV2Async(otherUser, + [organizationUserRepository.UpdateForKeyRotation(otherUser.Id, [otherOrganizationUser])]); + + // Act - the rotating user submits another member's membership, carrying their own token + otherOrganizationUser.V2UpgradeToken = _otherV2UpgradeToken; + await userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, + [organizationUserRepository.UpdateForKeyRotation(user.Id, [otherOrganizationUser])]); + + // Assert - the UserId filter keeps the caller from writing onto a membership they do not own + var updated = await organizationUserRepository.GetByIdAsync(otherOrganizationUser.Id); + Assert.NotNull(updated); + Assert.Equal(_v2UpgradeToken, updated.V2UpgradeToken); + } } diff --git a/test/Infrastructure.IntegrationTest/DatabaseTransactionActionTestHelper.cs b/test/Infrastructure.IntegrationTest/DatabaseTransactionActionTestHelper.cs new file mode 100644 index 000000000000..7a308a100437 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/DatabaseTransactionActionTestHelper.cs @@ -0,0 +1,48 @@ +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.IntegrationTest; + +/// +/// Executes delegates in integration tests. +/// Opens a connection and transaction appropriate for the database provider, executes the actions, and commits. +/// +public static class DatabaseTransactionActionTestHelper +{ + public static Task ExecuteAsync(Database database, DatabaseTransactionAction action, + IServiceProvider serviceProvider) + => ExecuteAsync(database, [action], serviceProvider); + + public static async Task ExecuteAsync(Database database, IEnumerable actions, + IServiceProvider serviceProvider) + { + var isDapper = database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf; + var connection = isDapper + ? new SqlConnection(database.ConnectionString) + : serviceProvider.GetRequiredService().Database.GetDbConnection(); + + try + { + await connection.OpenAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + + foreach (var action in actions) + { + await action(connection, transaction); + } + + await transaction.CommitAsync(); + } + finally + { + if (isDapper) + { + await connection.DisposeAsync(); + } + } + } +} diff --git a/test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs index 444c5c525f80..f7720a793ec9 100644 --- a/test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs @@ -5,7 +5,6 @@ using Bit.Core.KeyManagement.Enums; using Bit.Core.KeyManagement.Kdf; using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.IntegrationTest.AdminConsole; @@ -771,7 +770,7 @@ public async Task UpdateUserKeyAndEncryptedDataV2Async_InvokesUpdateDataActions( user.RevisionDate = DateTime.UtcNow; var actionWasInvoked = false; - UpdateEncryptedDataForKeyRotation action = (_, _) => + DatabaseTransactionAction action = (_, _) => { actionWasInvoked = true; return Task.CompletedTask;