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:
Copy link
Member

@Lujeni Lujeni Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get(publickey) returns None if the key is missing, but the return type is str. This None will then be passed to the others functions (b64) and will will raise an opaque excepton. You should explicitly check and raise a meaningful error (e.g. BitwardenError("User has no public key")).

used_id = user_id if user_id else self.sync().Profile.Id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you kept the same naming variable user_id pls

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,
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a docstrings pls and return type is not annotated pls

rsa_public_key_new_user = b64decode(
self.api_client.get_public_key_for_user(new_user.UserId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's None here, get_public_key_for_user(None) falls back to
self.sync().Profile.Id )the calling user's own public key?)

This means the organization key would be encrypted for the wrong user, potentially leaking it ?

)
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-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"]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VAULTWARDEN_INVITATIONS_ALLOWED is already explicitly set in the test script, do we really need to recheck it here?


Not related to this MR, we consider refactoring the entire test setup so we can more easily handle cases where we want to run the same tests with different variables. Ideally, this would integrate cleanly with pytest.mark and testcontainers.

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):
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": "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": []
}
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": "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": []
}