From d78b118f3d7734f318b3d6a2e680b560168ccbdd Mon Sep 17 00:00:00 2001 From: Lyonel Martinez Date: Mon, 22 Jul 2024 14:45:06 +0200 Subject: [PATCH 1/7] fix(typing): remove 'Optional' typing --- src/vaultwarden/clients/vaultwarden.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vaultwarden/clients/vaultwarden.py b/src/vaultwarden/clients/vaultwarden.py index 78e95c8..f38260f 100644 --- a/src/vaultwarden/clients/vaultwarden.py +++ b/src/vaultwarden/clients/vaultwarden.py @@ -1,6 +1,6 @@ import http from http.cookiejar import Cookie -from typing import Any, Literal, Optional +from typing import Any, Literal from uuid import UUID from httpx import Client, HTTPStatusError, Response @@ -43,7 +43,7 @@ def __init__( if preload_users: self._load_users() - def _get_admin_cookie(self) -> Optional[Cookie]: + def _get_admin_cookie(self) -> Cookie | None: """Get the session cookie, required to authenticate requests""" bw_cookies = ( c for c in self._http_client.cookies.jar if c.name == "VW_ADMIN" From c953de0f98e11074fdb9ec493e4e4dfd140e6493 Mon Sep 17 00:00:00 2001 From: Lyonel Martinez Date: Mon, 22 Jul 2024 15:12:29 +0200 Subject: [PATCH 2/7] fix(camelcase-api-field): Allow parsing of API result with camel-cased field follow vaultwarden 1.31 changes --- src/vaultwarden/models/bitwarden.py | 35 ++++++++++++++-------- src/vaultwarden/models/permissive_model.py | 13 ++++++++ src/vaultwarden/models/sync.py | 17 ++++++----- src/vaultwarden/utils/string_cases.py | 10 +++++++ 4 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 src/vaultwarden/models/permissive_model.py create mode 100644 src/vaultwarden/utils/string_cases.py diff --git a/src/vaultwarden/models/bitwarden.py b/src/vaultwarden/models/bitwarden.py index 19f755f..b550499 100644 --- a/src/vaultwarden/models/bitwarden.py +++ b/src/vaultwarden/models/bitwarden.py @@ -1,12 +1,19 @@ from typing import Generic, TypeVar from uuid import UUID -from pydantic import BaseModel, Field, TypeAdapter, field_validator +from pydantic import ( + BaseModel, + Field, + TypeAdapter, + field_validator, + AliasChoices, +) from pydantic_core.core_schema import FieldValidationInfo from vaultwarden.clients.bitwarden import BitwardenAPIClient 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 # Pydantic models for Bitwarden data structures @@ -14,13 +21,11 @@ T = TypeVar("T", bound="BitwardenBaseModel") -class ResplistBitwarden(BaseModel, Generic[T]): - Data: list[T] +class ResplistBitwarden(BaseModel, Generic[T], extra="allow"): + Data: list[T] = Field(alias="data", default=[]) -class BitwardenBaseModel( - BaseModel, extra="allow", arbitrary_types_allowed=True -): +class BitwardenBaseModel(PermissiveBaseModel): bitwarden_client: BitwardenAPIClient | None = Field( default=None, validate_default=True, exclude=True ) @@ -105,7 +110,11 @@ class CollectionAccess(BitwardenBaseModel): class CollectionUser(CollectionAccess): CollectionId: UUID | None = Field(None, validate_default=True) - UserId: UUID | None = Field(None, alias="Id", serialization_alias="Id") + UserId: UUID | None = Field( + None, + validation_alias=AliasChoices("id", "Id"), + serialization_alias="id", + ) @field_validator("CollectionId") @classmethod @@ -117,7 +126,9 @@ def set_id(cls, v, info: FieldValidationInfo): class UserCollection(CollectionAccess): CollectionId: UUID | None = Field( - None, alias="Id", serialization_alias="Id" + None, + validation_alias=AliasChoices("id", "Id"), + serialization_alias="id", ) UserId: UUID | None = Field(None, validate_default=True) @@ -133,7 +144,7 @@ class OrganizationCollection(BitwardenBaseModel): Id: UUID | None = None OrganizationId: UUID | None = Field(None, validate_default=True) Name: str - ExternalId: str | None + ExternalId: str | None = None @field_validator("OrganizationId") @classmethod @@ -484,9 +495,9 @@ def collections( def create_collection(self, name: str) -> OrganizationCollection: org_key = self.key() data = { - "Name": encrypt(2, name, self.key()), - "Groups": [], - "Users": [], + "name": encrypt(2, name, self.key()), + "groups": [], + "users": [], } resp = self.api_client.api_request( "POST", f"api/organizations/{self.Id}/collections", json=data diff --git a/src/vaultwarden/models/permissive_model.py b/src/vaultwarden/models/permissive_model.py new file mode 100644 index 0000000..9839a0c --- /dev/null +++ b/src/vaultwarden/models/permissive_model.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + +from vaultwarden.utils.string_cases import pascal_case_to_camel_case + + +class PermissiveBaseModel( + BaseModel, + extra="allow", + alias_generator=pascal_case_to_camel_case, + populate_by_name=True, + arbitrary_types_allowed=True, +): + pass diff --git a/src/vaultwarden/models/sync.py b/src/vaultwarden/models/sync.py index af2c088..2d2b802 100644 --- a/src/vaultwarden/models/sync.py +++ b/src/vaultwarden/models/sync.py @@ -1,13 +1,14 @@ import time from uuid import UUID -from pydantic import BaseModel, Field, field_validator +from pydantic import Field, field_validator, AliasChoices from vaultwarden.models.enum import VaultwardenUserStatus from vaultwarden.utils.crypto import decrypt +from vaultwarden.models.permissive_model import PermissiveBaseModel -class ConnectToken(BaseModel, extra="allow"): +class ConnectToken(PermissiveBaseModel): Kdf: int = 0 KdfIterations: int = 0 KdfMemory: int | None = None @@ -44,7 +45,7 @@ def orgs_key(self): return decrypt(self.PrivateKey, self.user_key) -class ProfileOrganization(BaseModel, extra="allow"): +class ProfileOrganization(PermissiveBaseModel): Id: UUID Name: str Key: str | None = None @@ -67,7 +68,7 @@ class ProfileOrganization(BaseModel, extra="allow"): UseTotp: bool -class UserProfile(BaseModel, extra="allow"): +class UserProfile(PermissiveBaseModel): AvatarColor: str | None Culture: str Email: str @@ -85,7 +86,9 @@ class UserProfile(BaseModel, extra="allow"): Providers: list = [] SecurityStamp: str TwoFactorEnabled: bool - status: VaultwardenUserStatus = Field(alias="_Status") + status: VaultwardenUserStatus = Field( + validation_alias=AliasChoices("_status", "_Status") + ) class VaultwardenUser(UserProfile): @@ -95,10 +98,10 @@ class VaultwardenUser(UserProfile): # TODO: add definition of attribute's types -class SyncData(BaseModel, extra="allow"): +class SyncData(PermissiveBaseModel): Ciphers: list[dict] = [] Collections: list[dict] = [] - Domains: dict = {} + Domains: dict | None = {} Folders: list[dict] = [] Policies: list[dict] = [] Profile: UserProfile diff --git a/src/vaultwarden/utils/string_cases.py b/src/vaultwarden/utils/string_cases.py new file mode 100644 index 0000000..a23fc65 --- /dev/null +++ b/src/vaultwarden/utils/string_cases.py @@ -0,0 +1,10 @@ +def pascal_case_to_camel_case(pascal: str) -> str: + """Convert a PascalCase string to camelCase. + + Args: + pascal: The string to convert. + + Returns: + The converted camelCase string. + """ + return pascal[0].lower() + pascal[1:] From 89ba5ab0c5800fe83a789d661fca404eab52d835 Mon Sep 17 00:00:00 2001 From: Lyonel Martinez Date: Mon, 22 Jul 2024 15:26:57 +0200 Subject: [PATCH 3/7] fix(default-values): remove mutable default values --- src/vaultwarden/models/bitwarden.py | 2 +- src/vaultwarden/models/sync.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/vaultwarden/models/bitwarden.py b/src/vaultwarden/models/bitwarden.py index b550499..402fd03 100644 --- a/src/vaultwarden/models/bitwarden.py +++ b/src/vaultwarden/models/bitwarden.py @@ -2,11 +2,11 @@ from uuid import UUID from pydantic import ( + AliasChoices, BaseModel, Field, TypeAdapter, field_validator, - AliasChoices, ) from pydantic_core.core_schema import FieldValidationInfo diff --git a/src/vaultwarden/models/sync.py b/src/vaultwarden/models/sync.py index 2d2b802..ed12f66 100644 --- a/src/vaultwarden/models/sync.py +++ b/src/vaultwarden/models/sync.py @@ -1,11 +1,11 @@ import time from uuid import UUID -from pydantic import Field, field_validator, AliasChoices +from pydantic import AliasChoices, Field, field_validator from vaultwarden.models.enum import VaultwardenUserStatus -from vaultwarden.utils.crypto import decrypt from vaultwarden.models.permissive_model import PermissiveBaseModel +from vaultwarden.utils.crypto import decrypt class ConnectToken(PermissiveBaseModel): @@ -79,11 +79,11 @@ class UserProfile(PermissiveBaseModel): MasterPasswordHint: str | None Name: str Object: str | None - Organizations: list[ProfileOrganization] = [] + Organizations: list[ProfileOrganization] Premium: bool PrivateKey: str | None - ProviderOrganizations: list = [] - Providers: list = [] + ProviderOrganizations: list + Providers: list SecurityStamp: str TwoFactorEnabled: bool status: VaultwardenUserStatus = Field( @@ -99,10 +99,10 @@ class VaultwardenUser(UserProfile): # TODO: add definition of attribute's types class SyncData(PermissiveBaseModel): - Ciphers: list[dict] = [] - Collections: list[dict] = [] - Domains: dict | None = {} - Folders: list[dict] = [] - Policies: list[dict] = [] + Ciphers: list[dict] + Collections: list[dict] + Domains: dict | None + Folders: list[dict] + Policies: list[dict] Profile: UserProfile - Sends: list[dict] = [] + Sends: list[dict] From fd6ac52f43136c6d378bf36acbb2d6162881952d Mon Sep 17 00:00:00 2001 From: Lyonel Martinez Date: Mon, 22 Jul 2024 15:30:54 +0200 Subject: [PATCH 4/7] fix(typing): fix mypy tiping and checks --- src/vaultwarden/models/bitwarden.py | 4 ++-- src/vaultwarden/models/sync.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vaultwarden/models/bitwarden.py b/src/vaultwarden/models/bitwarden.py index 402fd03..d07d10d 100644 --- a/src/vaultwarden/models/bitwarden.py +++ b/src/vaultwarden/models/bitwarden.py @@ -230,7 +230,7 @@ def add_collections(self, collections: list[UUID]): if collection in _current_collections: continue user = UserCollection( - Id=collection, + CollectionId=collection, UserId=self.Id, ReadOnly=False, HidePasswords=False, @@ -295,7 +295,7 @@ def update_collection(self, collections: list[UUID]): self.Collections = [ UserCollection( UserId=self.Id, - Id=coll, + CollectionId=coll, ReadOnly=False, HidePasswords=False, ) diff --git a/src/vaultwarden/models/sync.py b/src/vaultwarden/models/sync.py index ed12f66..4978eda 100644 --- a/src/vaultwarden/models/sync.py +++ b/src/vaultwarden/models/sync.py @@ -32,9 +32,7 @@ def expires_in_to_time(cls, v): def is_expired(self, now=None): if now is None: now = time.time() - if (self.expires_in is not None) and (self.expires_in <= now): - return True - return False + return (self.expires_in is not None) and (self.expires_in <= now) @property def user_key(self): From f87b0c2e77c358b5dd57a6252b5fcbb4506ea6a2 Mon Sep 17 00:00:00 2001 From: Lyonel Martinez Date: Tue, 23 Jul 2024 15:52:44 +0200 Subject: [PATCH 5/7] fix(bitwarden): fix List model and refresh master_ke when refresh token --- src/vaultwarden/clients/bitwarden.py | 6 ++++++ src/vaultwarden/models/bitwarden.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vaultwarden/clients/bitwarden.py b/src/vaultwarden/clients/bitwarden.py index 9e48b09..8c8858e 100644 --- a/src/vaultwarden/clients/bitwarden.py +++ b/src/vaultwarden/clients/bitwarden.py @@ -67,6 +67,12 @@ def _refresh_connect_token(self): ) self._connect_token = ConnectToken.model_validate_json(resp.text) + self._connect_token.master_key = make_master_key( + password=self.password, + salt=self.email, + iterations=self._connect_token.KdfIterations, + ) + def _set_connect_token(self): headers = { "content-type": "application/x-www-form-urlencoded; charset=utf-8", diff --git a/src/vaultwarden/models/bitwarden.py b/src/vaultwarden/models/bitwarden.py index d07d10d..1228a39 100644 --- a/src/vaultwarden/models/bitwarden.py +++ b/src/vaultwarden/models/bitwarden.py @@ -21,8 +21,8 @@ T = TypeVar("T", bound="BitwardenBaseModel") -class ResplistBitwarden(BaseModel, Generic[T], extra="allow"): - Data: list[T] = Field(alias="data", default=[]) +class ResplistBitwarden(PermissiveBaseModel, Generic[T]): + Data: list[T] class BitwardenBaseModel(PermissiveBaseModel): From f65af8442092016bb594a7afac5e3e66096179e0 Mon Sep 17 00:00:00 2001 From: Lyonel Martinez Date: Wed, 24 Jul 2024 09:35:35 +0200 Subject: [PATCH 6/7] fix(lint): remove unused import --- src/vaultwarden/models/bitwarden.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vaultwarden/models/bitwarden.py b/src/vaultwarden/models/bitwarden.py index 1228a39..5d3653a 100644 --- a/src/vaultwarden/models/bitwarden.py +++ b/src/vaultwarden/models/bitwarden.py @@ -3,7 +3,6 @@ from pydantic import ( AliasChoices, - BaseModel, Field, TypeAdapter, field_validator, From 8bcd44ff7607d0c5cd9663a338d3179105f99ed0 Mon Sep 17 00:00:00 2001 From: Lyonel Martinez Date: Thu, 25 Jul 2024 14:03:16 +0200 Subject: [PATCH 7/7] fix(lint): import fix --- src/vaultwarden/models/bitwarden.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/vaultwarden/models/bitwarden.py b/src/vaultwarden/models/bitwarden.py index 5d3653a..df7ec7d 100644 --- a/src/vaultwarden/models/bitwarden.py +++ b/src/vaultwarden/models/bitwarden.py @@ -1,12 +1,7 @@ from typing import Generic, TypeVar from uuid import UUID -from pydantic import ( - AliasChoices, - Field, - TypeAdapter, - field_validator, -) +from pydantic import AliasChoices, Field, TypeAdapter, field_validator from pydantic_core.core_schema import FieldValidationInfo from vaultwarden.clients.bitwarden import BitwardenAPIClient