diff --git a/src/vaultwarden/clients/bitwarden.py b/src/vaultwarden/clients/bitwarden.py index 8c8858e..f4376d7 100644 --- a/src/vaultwarden/clients/bitwarden.py +++ b/src/vaultwarden/clients/bitwarden.py @@ -132,6 +132,11 @@ def _api_request( method, path, headers=headers, **kwargs ) + def get_public_key_for_user(self, user_id: UUID | None = None) -> str: + used_id = user_id if user_id else self.sync().Profile.Id + resp = self.api_request("GET", f"api/users/{used_id}/public-key") + return resp.json().get("publicKey") + def sync(self, force_refresh: bool = False) -> SyncData: if self._sync is None or force_refresh: resp = self._api_request("GET", "api/sync") diff --git a/src/vaultwarden/models/bitwarden.py b/src/vaultwarden/models/bitwarden.py index 8a092c6..e8c87d3 100644 --- a/src/vaultwarden/models/bitwarden.py +++ b/src/vaultwarden/models/bitwarden.py @@ -1,3 +1,4 @@ +from base64 import b64decode from typing import Generic, Literal, TypeVar, cast from uuid import UUID @@ -8,7 +9,7 @@ from vaultwarden.models.enum import CipherType, OrganizationUserType from vaultwarden.models.exception_models import BitwardenError from vaultwarden.models.permissive_model import PermissiveBaseModel -from vaultwarden.utils.crypto import decrypt, encrypt +from vaultwarden.utils.crypto import decrypt, encrypt, encrypt_asym # Pydantic models for Bitwarden data structures @@ -423,6 +424,27 @@ def invite( self._users = self._get_users() return resp + def confirm( + self, + new_user: OrganizationUserDetails, + ): + rsa_public_key_new_user = b64decode( + self.api_client.get_public_key_for_user(new_user.UserId) + ) + org_key_decrypted = self.key() + key = encrypt_asym(org_key_decrypted, rsa_public_key_new_user) + + payload = { + "key": key, + } + resp = self.api_client.api_request( + "POST", + f"api/organizations/{self.Id}/users/{new_user.Id}/confirm", + json=payload, + ) + self._users = self._get_users() + return resp + def _get_users(self) -> list[OrganizationUserDetails]: resp = self.api_client.api_request( "GET", diff --git a/tests/e2e/run_tests.sh b/tests/e2e/run_tests.sh index 4059ef8..e8bca15 100755 --- a/tests/e2e/run_tests.sh +++ b/tests/e2e/run_tests.sh @@ -4,13 +4,15 @@ if [[ -z "${VAULTWARDEN_VERSION}" ]]; then VAULTWARDEN_VERSION="1.34.3" fi +export VAULTWARDEN_INVITATIONS_ALLOWED="false" + temp_dir=$(mktemp -d) # Copy fixtures db to tmp cp tests/fixtures/server/* $temp_dir # Start Vaultwarden docker -docker run -d --name vaultwarden -v $temp_dir:/data --env I_REALLY_WANT_VOLATILE_STORAGE=true --env ADMIN_TOKEN=admin --restart unless-stopped -p 80:80 vaultwarden/server:${VAULTWARDEN_VERSION} +docker run -d --name vaultwarden -v $temp_dir:/data --env INVITATIONS_ALLOWED=${VAULTWARDEN_INVITATIONS_ALLOWED} --env I_REALLY_WANT_VOLATILE_STORAGE=true --env ADMIN_TOKEN=admin --restart unless-stopped -p 80:80 vaultwarden/server:${VAULTWARDEN_VERSION} exit 0 diff --git a/tests/e2e/test_bitwarden.py b/tests/e2e/test_bitwarden.py index ff59ad5..4968955 100644 --- a/tests/e2e/test_bitwarden.py +++ b/tests/e2e/test_bitwarden.py @@ -97,12 +97,19 @@ def test_add_remove_collection_from_user(self): ) def test_invite_user_than_remove(self): - resp = self.organization.invite("test-user-3@example.com") + resp = self.organization.invite("test-account-3@example.com") self.assertTrue(resp.is_success) + + if not os.environ.get("VAULTWARDEN_INVITATIONS_ALLOWED", "True").lower() in ["true", "1", "yes"]: + user = self.organization.user_search( + "test-account-3@example.com", force_refresh=True + ) + resp = self.organization.confirm(user) + self.assertTrue(resp.is_success) + user = self.organization.user_search( - "test-user-3@example.com", force_refresh=True + "test-account-3@example.com", force_refresh=True ) - self.assertIsNotNone(user) user.delete() def test_rename_organization(self): diff --git a/tests/fixtures/server/db.sqlite3 b/tests/fixtures/server/db.sqlite3 index bf27515..397d181 100644 --- a/tests/fixtures/server/db.sqlite3 +++ b/tests/fixtures/server/db.sqlite3 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8c11a331ba097f1644b297885cd09b4ac5fc975ed5605176c880bc5cf08a813 -size 245760 +oid sha256:7e5a0c79f8493d676efea0d58a960f9cb49379960cf9a52ca20c2991707380a9 +size 258048 diff --git a/tests/fixtures/test-account-3/sync_camel.json b/tests/fixtures/test-account-3/sync_camel.json new file mode 100644 index 0000000..32e8847 --- /dev/null +++ b/tests/fixtures/test-account-3/sync_camel.json @@ -0,0 +1,81 @@ +{ + "ciphers": [ + { + "attachments": null, + "card": null, + "collectionIds": [], + "creationDate": "2025-09-16T09:11:05.230147Z", + "data": { + "autofillOnPageLoad": null, + "fields": [], + "name": "2.lxfHdjXa8Ftxs2vjBl9LUg==|eHY6cfrUzOex48QTCul2BA==|ZC8sHtnTeY5n9ZZ3hfOtlvS2iCSkXsdZ6W/pQFu0qJo=", + "notes": null, + "password": "2.rEd5dQ7mga3JTFRlyavoOQ==|s06bxxDijrDhsfZEwoFGUA==|TUzsgYoHFNuQ8ZIkFI7N6Xk3JSANAWypnRA0psvaVHo=", + "passwordHistory": [], + "passwordRevisionDate": null, + "totp": null, + "uri": null, + "uris": [], + "username": "2.1C+RTMihhirz1a998m+gHQ==|vyrXYEzsqHX3ZA8Prl4WeQ==|Wl/A/quNuDQHn3iE0JTQNdo0mlMEIf0txxo4LkX+SeY=" + }, + "deletedDate": null, + "edit": true, + "favorite": false, + "fields": [], + "folderId": null, + "id": "40b40f86-6899-45b1-8c18-82ab54ba42e5", + "identity": null, + "key": null, + "login": { + "autofillOnPageLoad": null, + "password": "2.rEd5dQ7mga3JTFRlyavoOQ==|s06bxxDijrDhsfZEwoFGUA==|TUzsgYoHFNuQ8ZIkFI7N6Xk3JSANAWypnRA0psvaVHo=", + "passwordRevisionDate": null, + "totp": null, + "uri": null, + "uris": [], + "username": "2.1C+RTMihhirz1a998m+gHQ==|vyrXYEzsqHX3ZA8Prl4WeQ==|Wl/A/quNuDQHn3iE0JTQNdo0mlMEIf0txxo4LkX+SeY=" + }, + "name": "2.lxfHdjXa8Ftxs2vjBl9LUg==|eHY6cfrUzOex48QTCul2BA==|ZC8sHtnTeY5n9ZZ3hfOtlvS2iCSkXsdZ6W/pQFu0qJo=", + "notes": null, + "object": "cipherDetails", + "organizationId": null, + "organizationUseTotp": true, + "passwordHistory": [], + "permissions": { "delete": true, "restore": true }, + "reprompt": 0, + "revisionDate": "2025-09-16T09:11:05.230927Z", + "secureNote": null, + "sshKey": null, + "type": 1, + "viewPassword": true + } + ], + "collections": [], + "domains": null, + "folders": [], + "object": "sync", + "policies": [], + "profile": { + "_status": 0, + "avatarColor": null, + "creationDate": "2025-09-16T09:10:24.842870Z", + "culture": "en-US", + "email": "test-account-3@example.com", + "emailVerified": true, + "forcePasswordReset": false, + "id": "340e2a49-a7d6-401b-a7ba-47b1e96f85e9", + "key": "2.igh2s8pfm4iIdEeU0Kfqeg==|Hau9L5R0hmnLVj9DQhW4g7ACmWklmC+KE/qvdOM2f+r+c0fAGc+SKHZixxPmVfX94trKSxfpa9ubFTLcxbDSiKE23ZXLJ5+uLIaUORBVeuY=|gGZeOxQTdzNQTbWL0jp50q4iOtsFNyUSPz9sb3hrgmA=", + "name": "test-account-3", + "object": "profile", + "organizations": [], + "premium": true, + "premiumFromOrganization": false, + "privateKey": "2.F9u34xZd5pPqt9qRzmMwZg==|2bn5RoJW7ePPZZ0ekbsRWmTOpGnPehEguv9f5L7wdYN8onaxdwNl87mIxv1whSDMD4+/OYr+Hlsm0/FvYY+fjmFgyAjnWt1ZQMrtFY8Y8Xqdvl7GlC0ocnLu8p6cJBqw4ezMOe1J6duxECvoTLc5t8EP0NV+KYu++doe8AZIZ0bwvH8s5RDLG2xrRjEGqX+QotEWxrSZtX6oEgDmwz873/Q/lGZE9nauAixqQ6XFC8ylT/CZVxEH71X7NvXS/1UyEOyR2ubJ2sR+HZRfCndKmNvFQHszYIFgzYF9/srqFVK5tvD0CZsqGwbvbUrutYHQLcUDQcjBn28hWIsmahY2ZJG4LSdg8y4SdjiLg05M34cMoNzT5OWwDGjUHoSIpmlJ5UNg3gxW/8kh2ZzMQuYvx5CA0CQpSuoGvSwmrBVmzGX0NySZjQcvbZchIekCbIPyc5v7cwvXoR7dtoEHm5TBq1pW2LDCccaGM49ziFlrhLNal2qzYHGIIdMqdAVkxeWzfK0N5H58MomjPiUHlb+PByyxo65+1yImpn2m2RT04ovxponXCLOl9w/CxiXxzHM1wXOfZlT8rnVfi09OBXt4358H6Tr6BCcvJxLZ6DPLyrmiy7D3zWCWAd5CeyACmT2yRHUF4m5uPy9sv95dDC3GOFvpJEAhYqCR2haNN1ziCk/fnSZdgalsJSuV7jk9RkkrOl+CziYaXjwZOH91pTAne1k0bGCJFVoa3uhREkkNUVcFO3fZye1V2SFaF4oYUScyY6uY2Df4c7U8T0P/YoZ5u0g0SpJ4nWoY0Lzn2T9cV5d1T5VejGAAa2lOyjqQ6kIpgC6v4CMddOKAwJj/yEB9iWGnrJmtWkp6v70MQAsWCT1QCBeIMnvzUL80QjecbwC2NFHCUw8j4VeCmCxWS9iEZuZUXy2RawwJZISiDLXr3ZxfcvbCzAGWQg0m1b6yapDSzr4EtKBgR75l8bhhJA+WzQ8QWAaBQwAz3nmBywzPrxJVlxQPqQYRsZroLfCDJusDe9aTzmGaCzdu/nj/m3qAbYilmhChbg/fL4sy5O48eGoc0CyoaOFdb4kQ7bo+USmlCRFqhSjgBiKxvVU6P36SDDyTTTvG+14tdc1yXmM7oQywD85gTQBQrlh5QbmMslTeNzFLUpV9lNbaDECc5z56M//UW36Bu2kID8p+BbqFkv40epz5K4Hi+ebsKKyHe3FwiMltZAEAMMxQycooZDkKwrvpEjd07WpkmjMDmarhPj2GOx9ChcK0gib1ee7nEh5/p2csTf4/KzAGyL/bGhHs3iNmLULu/7SL0Shitf7r9ZaSmYyu05Hl/qSdZ77NMvckHvABnHTb3b6QVCtiDywaKixqoUsDV7v58c5+gXcZ57Nb2oFyA9JjWYyNAZ3EzlqOK/P9pO0AfV+SLzo/FpZVSumP4JomfpTOq6NweXrqJlMqNA6i0I9/rR/ZxaSMdW50WHJVNK6XrQroa129iofNqYpkPCZX0ir6dd9dUQUhajq1zgdsoqjIbX8EM6VSJ6ghPmKz4LuZp6f7S9r1QYNjPUfEf+POrGvpMGG+Tov42J9QEW4I7xTCyztMqY/3KjOtYpjvA1z1DJ5N6rZk+uwK5E+1tlb7lCjVf+pqMjSERAw=|kuTGtWGPv3crTSNid5pRuWiGs2VP8JFx2W1CjROkq0A=", + "providerOrganizations": [], + "providers": [], + "securityStamp": "6377d30a-fe24-4c71-b9ef-18cc568ee915", + "twoFactorEnabled": false, + "usesKeyConnector": false + }, + "sends": [] +} diff --git a/tests/fixtures/test-account-3/sync_pascal.json b/tests/fixtures/test-account-3/sync_pascal.json new file mode 100644 index 0000000..2a435ee --- /dev/null +++ b/tests/fixtures/test-account-3/sync_pascal.json @@ -0,0 +1,84 @@ +{ + "Ciphers": [ + { + "Attachments": null, + "Card": null, + "CollectionIds": [], + "CreationDate": "2025-09-16T09:11:05.230147Z", + "Data": { + "AutofillOnPageLoad": null, + "Fields": [], + "Name": "2.lxfHdjXa8Ftxs2vjBl9LUg==|eHY6cfrUzOex48QTCul2BA==|ZC8sHtnTeY5n9ZZ3hfOtlvS2iCSkXsdZ6W\/pQFu0qJo=", + "Notes": null, + "Password": "2.rEd5dQ7mga3JTFRlyavoOQ==|s06bxxDijrDhsfZEwoFGUA==|TUzsgYoHFNuQ8ZIkFI7N6Xk3JSANAWypnRA0psvaVHo=", + "PasswordHistory": [], + "PasswordRevisionDate": null, + "Totp": null, + "Uri": null, + "Uris": [], + "Username": "2.1C+RTMihhirz1a998m+gHQ==|vyrXYEzsqHX3ZA8Prl4WeQ==|Wl\/A\/quNuDQHn3iE0JTQNdo0mlMEIf0txxo4LkX+SeY=" + }, + "DeletedDate": null, + "Edit": true, + "Favorite": false, + "Fields": [], + "FolderId": null, + "Id": "40b40f86-6899-45b1-8c18-82ab54ba42e5", + "Identity": null, + "Key": null, + "Login": { + "AutofillOnPageLoad": null, + "Password": "2.rEd5dQ7mga3JTFRlyavoOQ==|s06bxxDijrDhsfZEwoFGUA==|TUzsgYoHFNuQ8ZIkFI7N6Xk3JSANAWypnRA0psvaVHo=", + "PasswordRevisionDate": null, + "Totp": null, + "Uri": null, + "Uris": [], + "Username": "2.1C+RTMihhirz1a998m+gHQ==|vyrXYEzsqHX3ZA8Prl4WeQ==|Wl\/A\/quNuDQHn3iE0JTQNdo0mlMEIf0txxo4LkX+SeY=" + }, + "Name": "2.lxfHdjXa8Ftxs2vjBl9LUg==|eHY6cfrUzOex48QTCul2BA==|ZC8sHtnTeY5n9ZZ3hfOtlvS2iCSkXsdZ6W\/pQFu0qJo=", + "Notes": null, + "Object": "cipherDetails", + "OrganizationId": null, + "OrganizationUseTotp": true, + "PasswordHistory": [], + "Permissions": { + "Delete": true, + "Restore": true + }, + "Reprompt": 0, + "RevisionDate": "2025-09-16T09:11:05.230927Z", + "SecureNote": null, + "SshKey": null, + "Type": 1, + "ViewPassword": true + } + ], + "Collections": [], + "Domains": null, + "Folders": [], + "Object": "sync", + "Policies": [], + "Profile": { + "Status": 0, + "AvatarColor": null, + "CreationDate": "2025-09-16T09:10:24.842870Z", + "Culture": "en-US", + "Email": "test-account-3@example.com", + "EmailVerified": true, + "ForcePasswordReset": false, + "Id": "340e2a49-a7d6-401b-a7ba-47b1e96f85e9", + "Key": "2.igh2s8pfm4iIdEeU0Kfqeg==|Hau9L5R0hmnLVj9DQhW4g7ACmWklmC+KE\/qvdOM2f+r+c0fAGc+SKHZixxPmVfX94trKSxfpa9ubFTLcxbDSiKE23ZXLJ5+uLIaUORBVeuY=|gGZeOxQTdzNQTbWL0jp50q4iOtsFNyUSPz9sb3hrgmA=", + "Name": "test-account-3", + "Object": "profile", + "Organizations": [], + "Premium": true, + "PremiumFromOrganization": false, + "PrivateKey": "2.F9u34xZd5pPqt9qRzmMwZg==|2bn5RoJW7ePPZZ0ekbsRWmTOpGnPehEguv9f5L7wdYN8onaxdwNl87mIxv1whSDMD4+\/OYr+Hlsm0\/FvYY+fjmFgyAjnWt1ZQMrtFY8Y8Xqdvl7GlC0ocnLu8p6cJBqw4ezMOe1J6duxECvoTLc5t8EP0NV+KYu++doe8AZIZ0bwvH8s5RDLG2xrRjEGqX+QotEWxrSZtX6oEgDmwz873\/Q\/lGZE9nauAixqQ6XFC8ylT\/CZVxEH71X7NvXS\/1UyEOyR2ubJ2sR+HZRfCndKmNvFQHszYIFgzYF9\/srqFVK5tvD0CZsqGwbvbUrutYHQLcUDQcjBn28hWIsmahY2ZJG4LSdg8y4SdjiLg05M34cMoNzT5OWwDGjUHoSIpmlJ5UNg3gxW\/8kh2ZzMQuYvx5CA0CQpSuoGvSwmrBVmzGX0NySZjQcvbZchIekCbIPyc5v7cwvXoR7dtoEHm5TBq1pW2LDCccaGM49ziFlrhLNal2qzYHGIIdMqdAVkxeWzfK0N5H58MomjPiUHlb+PByyxo65+1yImpn2m2RT04ovxponXCLOl9w\/CxiXxzHM1wXOfZlT8rnVfi09OBXt4358H6Tr6BCcvJxLZ6DPLyrmiy7D3zWCWAd5CeyACmT2yRHUF4m5uPy9sv95dDC3GOFvpJEAhYqCR2haNN1ziCk\/fnSZdgalsJSuV7jk9RkkrOl+CziYaXjwZOH91pTAne1k0bGCJFVoa3uhREkkNUVcFO3fZye1V2SFaF4oYUScyY6uY2Df4c7U8T0P\/YoZ5u0g0SpJ4nWoY0Lzn2T9cV5d1T5VejGAAa2lOyjqQ6kIpgC6v4CMddOKAwJj\/yEB9iWGnrJmtWkp6v70MQAsWCT1QCBeIMnvzUL80QjecbwC2NFHCUw8j4VeCmCxWS9iEZuZUXy2RawwJZISiDLXr3ZxfcvbCzAGWQg0m1b6yapDSzr4EtKBgR75l8bhhJA+WzQ8QWAaBQwAz3nmBywzPrxJVlxQPqQYRsZroLfCDJusDe9aTzmGaCzdu\/nj\/m3qAbYilmhChbg\/fL4sy5O48eGoc0CyoaOFdb4kQ7bo+USmlCRFqhSjgBiKxvVU6P36SDDyTTTvG+14tdc1yXmM7oQywD85gTQBQrlh5QbmMslTeNzFLUpV9lNbaDECc5z56M\/\/UW36Bu2kID8p+BbqFkv40epz5K4Hi+ebsKKyHe3FwiMltZAEAMMxQycooZDkKwrvpEjd07WpkmjMDmarhPj2GOx9ChcK0gib1ee7nEh5\/p2csTf4\/KzAGyL\/bGhHs3iNmLULu\/7SL0Shitf7r9ZaSmYyu05Hl\/qSdZ77NMvckHvABnHTb3b6QVCtiDywaKixqoUsDV7v58c5+gXcZ57Nb2oFyA9JjWYyNAZ3EzlqOK\/P9pO0AfV+SLzo\/FpZVSumP4JomfpTOq6NweXrqJlMqNA6i0I9\/rR\/ZxaSMdW50WHJVNK6XrQroa129iofNqYpkPCZX0ir6dd9dUQUhajq1zgdsoqjIbX8EM6VSJ6ghPmKz4LuZp6f7S9r1QYNjPUfEf+POrGvpMGG+Tov42J9QEW4I7xTCyztMqY\/3KjOtYpjvA1z1DJ5N6rZk+uwK5E+1tlb7lCjVf+pqMjSERAw=|kuTGtWGPv3crTSNid5pRuWiGs2VP8JFx2W1CjROkq0A=", + "ProviderOrganizations": [], + "Providers": [], + "SecurityStamp": "6377d30a-fe24-4c71-b9ef-18cc568ee915", + "TwoFactorEnabled": false, + "UsesKeyConnector": false + }, + "Sends": [] +} \ No newline at end of file