Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/vaultwarden/clients/bitwarden.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
24 changes: 23 additions & 1 deletion src/vaultwarden/models/bitwarden.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from base64 import b64decode
from typing import Generic, Literal, TypeVar, cast
from uuid import UUID

Expand All @@ -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

Expand Down Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 10 additions & 3 deletions tests/e2e/test_bitwarden.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[email protected]")
resp = self.organization.invite("test-account[email protected]")
self.assertTrue(resp.is_success)

if not os.environ.get("VAULTWARDEN_INVITATIONS_ALLOWED", "True").lower() in ["true", "1", "yes"]:
user = self.organization.user_search(
"[email protected]", force_refresh=True
)
resp = self.organization.confirm(user)
self.assertTrue(resp.is_success)

user = self.organization.user_search(
"test-user[email protected]", force_refresh=True
"test-account[email protected]", force_refresh=True
)
self.assertIsNotNone(user)
user.delete()

def test_rename_organization(self):
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/server/db.sqlite3
Git LFS file not shown
81 changes: 81 additions & 0 deletions tests/fixtures/test-account-3/sync_camel.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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": []
}
84 changes: 84 additions & 0 deletions tests/fixtures/test-account-3/sync_pascal.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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": []
}