diff --git a/src/onepassword/client.py b/src/onepassword/client.py index 0b3d319..290cca9 100644 --- a/src/onepassword/client.py +++ b/src/onepassword/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import weakref -from .core import UniffiCore +from .core import UniffiCore, InnerClient from .desktop_core import DesktopCore from .defaults import new_default_config, DesktopAuth from .secrets import Secrets @@ -34,12 +34,14 @@ async def authenticate( client_id = int(await core.init_client(config)) + inner_client = InnerClient(client_id, core, config) + authenticated_client = cls() - authenticated_client.secrets = Secrets(client_id, core) - authenticated_client.items = Items(client_id, core) - authenticated_client.vaults = Vaults(client_id, core) - authenticated_client.groups = Groups(client_id, core) + authenticated_client.secrets = Secrets(inner_client) + authenticated_client.items = Items(inner_client) + authenticated_client.vaults = Vaults(inner_client) + authenticated_client.groups = Groups(inner_client) authenticated_client._finalizer = weakref.finalize( cls, core.release_client, client_id diff --git a/src/onepassword/core.py b/src/onepassword/core.py index d47068a..490a75e 100644 --- a/src/onepassword/core.py +++ b/src/onepassword/core.py @@ -1,19 +1,45 @@ +from __future__ import annotations import json import platform -from typing import Protocol -from onepassword.errors import raise_typed_exception +from typing import Any, Protocol +from onepassword.desktop_core import DesktopCore +from onepassword.errors import raise_typed_exception, DesktopSessionExpiredException # In empirical tests, we determined that maximum message size that can cross the FFI boundary # is ~128MB. Past this limit, FFI will throw an error and the program will crash. # We set the limit to 50MB to be safe and consistent with the other SDKs (where this limit is 64MB), to be reconsidered upon further testing MESSAGE_LIMIT = 50 * 1024 * 1024 + class Core(Protocol): async def init_client(self, client_config: dict) -> str: ... async def invoke(self, invoke_config: dict) -> str: ... def invoke_sync(self, invoke_config: dict) -> str: ... def release_client(self, client_id: int) -> None: ... + +class InnerClient: + client_id: int + core: DesktopCore | UniffiCore + config: dict[str, Any] + + def __init__(self, client_id: int, core: "DesktopCore | UniffiCore", config: dict[str, any]): + self.client_id = client_id + self.core = core + self.config = config + + async def invoke(self, invoke_config: dict): + try: + return await self.core.invoke(invoke_config) + except DesktopSessionExpiredException: + new_client_id = await self.core.init_client(self.config) + self.client_id = new_client_id + invoke_config["invocation"]["clientId"] = self.client_id + return await self.core.invoke(invoke_config) + except Exception as e: + raise e + + class UniffiCore: def __init__(self): machine_arch = platform.machine().lower() diff --git a/src/onepassword/desktop_core.py b/src/onepassword/desktop_core.py index b0e1b5e..bdf318a 100644 --- a/src/onepassword/desktop_core.py +++ b/src/onepassword/desktop_core.py @@ -23,6 +23,13 @@ def find_1password_lib_path(): "/opt/1Password/libop_sdk_ipc_client.so", "/snap/bin/1password/libop_sdk_ipc_client.so", ] + elif os_name == "Windows": + locations = [ + str(Path.home() / r"AppData\Local\1Password\op_sdk_ipc_client.dll"), + r"C:\Program Files\1Password\app\8\op_sdk_ipc_client.dll", + r"C:\Program Files (x86)\1Password\app\8\op_sdk_ipc_client.dll", + str(Path.home() / r"AppData\Local\1Password\app\8\op_sdk_ipc_client.dll"), + ] else: raise OSError(f"Unsupported operating system: {os_name}") @@ -92,7 +99,9 @@ def call_shared_library(self, payload: str, operation_kind: str) -> bytes: success = parsed.get("success", False) if not success: - raise_typed_exception(Exception(str(payload))) + e = Exception(payload) + e.msg = payload + raise_typed_exception(e) return payload diff --git a/src/onepassword/errors.py b/src/onepassword/errors.py index df80c3a..811aebb 100644 --- a/src/onepassword/errors.py +++ b/src/onepassword/errors.py @@ -3,6 +3,12 @@ import json +class DesktopSessionExpiredException(Exception): + def __init__(self, message): + self.message = message + super().__init__(self.message) + + class RateLimitExceededException(Exception): def __init__(self, message): self.message = message @@ -18,7 +24,9 @@ def raise_typed_exception(e: Exception): error_name = typed_error.get("name") message = typed_error.get("message") - if error_name == "RateLimitExceeded": + if error_name == "DesktopSessionExpired": + raise DesktopSessionExpiredException(message) + elif error_name == "RateLimitExceeded": raise RateLimitExceededException(message) elif message is not None: raise Exception(message) diff --git a/src/onepassword/groups.py b/src/onepassword/groups.py index c3f4a7a..2d01959 100644 --- a/src/onepassword/groups.py +++ b/src/onepassword/groups.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from pydantic import TypeAdapter from .types import Group, GroupGetParams @@ -10,15 +10,14 @@ class Groups: The Groups API holds all the operations the SDK client can perform on 1Password groups. """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def get(self, group_id: str, group_params: GroupGetParams) -> Group: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "GroupsGet", "parameters": { diff --git a/src/onepassword/items.py b/src/onepassword/items.py index c6b6901..591622a 100644 --- a/src/onepassword/items.py +++ b/src/onepassword/items.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from typing import List from pydantic import TypeAdapter from .items_shares import ItemsShares @@ -21,20 +21,19 @@ class Items: The Items API holds all operations the SDK client can perform on 1Password items. """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core - self.shares = ItemsShares(client_id, core) - self.files = ItemsFiles(client_id, core) + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client + self.shares = ItemsShares(inner_client) + self.files = ItemsFiles(inner_client) async def create(self, params: ItemCreateParams) -> Item: """ Create a new item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsCreate", "parameters": {"params": params.model_dump(by_alias=True)}, @@ -52,10 +51,10 @@ async def create_all( """ Create items in batch, within a single vault. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsCreateAll", "parameters": { @@ -74,10 +73,10 @@ async def get(self, vault_id: str, item_id: str) -> Item: """ Get an item by vault and item ID """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsGet", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -93,10 +92,10 @@ async def get_all(self, vault_id: str, item_ids: List[str]) -> ItemsGetAllRespon """ Get items by vault and their item IDs. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsGetAll", "parameters": {"vault_id": vault_id, "item_ids": item_ids}, @@ -112,10 +111,10 @@ async def put(self, item: Item) -> Item: """ Update an existing item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsPut", "parameters": {"item": item.model_dump(by_alias=True)}, @@ -131,10 +130,10 @@ async def delete(self, vault_id: str, item_id: str) -> None: """ Delete an item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsDelete", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -151,10 +150,10 @@ async def delete_all( """ Delete items in batch, within a single vault. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsDeleteAll", "parameters": {"vault_id": vault_id, "item_ids": item_ids}, @@ -170,10 +169,10 @@ async def archive(self, vault_id: str, item_id: str) -> None: """ Archive an item. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsArchive", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -188,10 +187,10 @@ async def list(self, vault_id: str, *filters: ItemListFilter) -> List[ItemOvervi """ List items based on filters. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsList", "parameters": { diff --git a/src/onepassword/items_files.py b/src/onepassword/items_files.py index d8c56ff..6895226 100644 --- a/src/onepassword/items_files.py +++ b/src/onepassword/items_files.py @@ -1,24 +1,23 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from typing import List from pydantic import TypeAdapter from .types import DocumentCreateParams, FileAttributes, FileCreateParams, Item class ItemsFiles: - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def attach(self, item: Item, file_params: FileCreateParams) -> Item: """ Attach files to Items """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesAttach", "parameters": { @@ -37,10 +36,10 @@ async def read(self, vault_id: str, item_id: str, attr: FileAttributes) -> bytes """ Read file content from the Item """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesRead", "parameters": { @@ -60,10 +59,10 @@ async def delete(self, item: Item, section_id: str, field_id: str) -> Item: """ Delete a field file from Item using the section and field IDs """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesDelete", "parameters": { @@ -85,10 +84,10 @@ async def replace_document( """ Replace the document file within a document item """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsFilesReplaceDocument", "parameters": { diff --git a/src/onepassword/items_shares.py b/src/onepassword/items_shares.py index 715630d..704ad1d 100644 --- a/src/onepassword/items_shares.py +++ b/src/onepassword/items_shares.py @@ -1,14 +1,14 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY +from .core import InnerClient from typing import List from pydantic import TypeAdapter from .types import Item, ItemShareAccountPolicy, ItemShareParams, ValidRecipient class ItemsShares: - def __init__(self, client_id, core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def get_account_policy( self, vault_id: str, item_id: str @@ -16,10 +16,10 @@ async def get_account_policy( """ Get the item sharing policy of your account. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsSharesGetAccountPolicy", "parameters": {"vault_id": vault_id, "item_id": item_id}, @@ -37,10 +37,10 @@ async def validate_recipients( """ Validate the recipients of an item sharing link. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsSharesValidateRecipients", "parameters": { @@ -61,10 +61,10 @@ async def create( """ Create a new item sharing link. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "ItemsSharesCreate", "parameters": { diff --git a/src/onepassword/lib/aarch64/libop_uniffi_core.dylib b/src/onepassword/lib/aarch64/libop_uniffi_core.dylib index 430b993..ec96524 100755 Binary files a/src/onepassword/lib/aarch64/libop_uniffi_core.dylib and b/src/onepassword/lib/aarch64/libop_uniffi_core.dylib differ diff --git a/src/onepassword/lib/aarch64/libop_uniffi_core.so b/src/onepassword/lib/aarch64/libop_uniffi_core.so index 7f14693..b2dce11 100755 Binary files a/src/onepassword/lib/aarch64/libop_uniffi_core.so and b/src/onepassword/lib/aarch64/libop_uniffi_core.so differ diff --git a/src/onepassword/lib/x86_64/libop_uniffi_core.dylib b/src/onepassword/lib/x86_64/libop_uniffi_core.dylib index 1062d0f..6ebca40 100755 Binary files a/src/onepassword/lib/x86_64/libop_uniffi_core.dylib and b/src/onepassword/lib/x86_64/libop_uniffi_core.dylib differ diff --git a/src/onepassword/lib/x86_64/libop_uniffi_core.so b/src/onepassword/lib/x86_64/libop_uniffi_core.so index c9ec993..96944d1 100755 Binary files a/src/onepassword/lib/x86_64/libop_uniffi_core.so and b/src/onepassword/lib/x86_64/libop_uniffi_core.so differ diff --git a/src/onepassword/lib/x86_64/op_uniffi_core.dll b/src/onepassword/lib/x86_64/op_uniffi_core.dll index e08cb01..497d391 100644 Binary files a/src/onepassword/lib/x86_64/op_uniffi_core.dll and b/src/onepassword/lib/x86_64/op_uniffi_core.dll differ diff --git a/src/onepassword/secrets.py b/src/onepassword/secrets.py index ffbd7d5..8b46dfd 100644 --- a/src/onepassword/secrets.py +++ b/src/onepassword/secrets.py @@ -1,6 +1,6 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core, UniffiCore +from .core import InnerClient, UniffiCore from typing import List from pydantic import TypeAdapter from .types import GeneratePasswordResponse, PasswordRecipe, ResolveAllResponse @@ -9,21 +9,20 @@ class Secrets: """ The Secrets API includes all operations the SDK client can perform on secrets. - Use secret reference URIs to securely load secrets from 1Password: op:///[/]/ + Use secret reference URIs to securely load secrets from 1Password: `op:///[/]/` """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client async def resolve(self, secret_reference: str) -> str: """ Resolve returns the secret the provided secret reference points to. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "SecretsResolve", "parameters": {"secret_reference": secret_reference}, @@ -39,10 +38,10 @@ async def resolve_all(self, secret_references: List[str]) -> ResolveAllResponse: """ Resolve takes in a list of secret references and returns the secrets they point to or errors if any. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "SecretsResolveAll", "parameters": {"secret_references": secret_references}, diff --git a/src/onepassword/types.py b/src/onepassword/types.py index e0989e7..0119cc3 100644 --- a/src/onepassword/types.py +++ b/src/onepassword/types.py @@ -242,12 +242,47 @@ class Group(BaseModel): vault_access: Optional[List[VaultAccess]] = Field(alias="vaultAccess", default=None) +class GroupAccess(BaseModel): + """ + Represents a group's access to a 1Password vault. + This is used for granting permissions + """ + + group_id: str + """ + The group's ID + """ + permissions: int + """ + The group's set of permissions for the vault + """ + + class GroupGetParams(BaseModel): model_config = ConfigDict(populate_by_name=True) vault_permissions: Optional[bool] = Field(alias="vaultPermissions", default=None) +class GroupVaultAccess(BaseModel): + """ + Represents a group's access to a 1Password vault. + """ + + vault_id: str + """ + The vault's ID + """ + group_id: str + """ + The group's ID + """ + permissions: int + """ + The group's set of permissions for the vault + """ + + class ItemCategory(str, Enum): LOGIN = "Login" SECURENOTE = "SecureNote" @@ -385,7 +420,7 @@ class AutofillBehavior(str, Enum): Controls the auto-fill behavior of a website. - For more information, visit https://support.1password.com/autofill-behavior/ + For more information, visit """ ANYWHEREONWEBSITE = "AnywhereOnWebsite" @@ -417,7 +452,7 @@ class Website(BaseModel): """ The auto-fill behavior of the website - For more information, visit https://support.1password.com/autofill-behavior/ + For more information, visit """ @@ -1493,3 +1528,19 @@ class WordListType(str, Enum): """ Three (random) letter "words" """ + + +ARCHIVE_ITEMS: int = 256 +CREATE_ITEMS: int = 128 +DELETE_ITEMS: int = 512 +EXPORT_ITEMS: int = 4194304 +IMPORT_ITEMS: int = 2097152 +MANAGE_VAULT: int = 2 +NO_ACCESS: int = 0 +PRINT_ITEMS: int = 8388608 +READ_ITEMS: int = 32 +RECOVER_VAULT: int = 1 +REVEAL_ITEM_PASSWORD: int = 16 +SEND_ITEMS: int = 1048576 +UPDATE_ITEMS: int = 64 +UPDATE_ITEM_HISTORY: int = 1024 diff --git a/src/onepassword/vaults.py b/src/onepassword/vaults.py index 09d6adf..2664e33 100644 --- a/src/onepassword/vaults.py +++ b/src/onepassword/vaults.py @@ -1,9 +1,11 @@ # Code generated by op-codegen - DO NO EDIT MANUALLY -from .core import Core +from .core import InnerClient from typing import Optional, List from pydantic import TypeAdapter from .types import ( + GroupAccess, + GroupVaultAccess, Vault, VaultGetParams, VaultListParams, @@ -16,21 +18,27 @@ class Vaults: The Vaults API holds all the operations the SDK client can perform on 1Password vaults. """ - def __init__(self, client_id, core: Core): - self.client_id = client_id - self.core = core + def __init__(self, inner_client: InnerClient): + self.inner_client = inner_client - async def list(self, params: Optional[VaultListParams] = None) -> List[VaultOverview]: + async def list( + self, params: Optional[VaultListParams] = None + ) -> List[VaultOverview]: """ List information about vaults that's configurable based on some input parameters. """ - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsList", - "parameters": {"params": params.model_dump(by_alias=True) if params else None}, + "parameters": { + "params": ( + params.model_dump( + by_alias=True) if params else None + ) + }, }, } } @@ -40,10 +48,10 @@ async def list(self, params: Optional[VaultListParams] = None) -> List[VaultOver return response async def get_overview(self, vault_uuid: str) -> VaultOverview: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsGetOverview", "parameters": {"vault_uuid": vault_uuid}, @@ -56,10 +64,10 @@ async def get_overview(self, vault_uuid: str) -> VaultOverview: return response async def get(self, vault_uuid: str, vault_params: VaultGetParams) -> Vault: - response = await self.core.invoke( + response = await self.inner_client.invoke( { "invocation": { - "clientId": self.client_id, + "clientId": self.inner_client.client_id, "parameters": { "name": "VaultsGet", "parameters": { @@ -73,3 +81,63 @@ async def get(self, vault_uuid: str, vault_params: VaultGetParams) -> Vault: response = TypeAdapter(Vault).validate_json(response) return response + + async def grant_group_permissions( + self, vault_id: str, group_permissions_list: List[GroupAccess] + ) -> None: + response = await self.inner_client.invoke( + { + "invocation": { + "clientId": self.inner_client.client_id, + "parameters": { + "name": "VaultsGrantGroupPermissions", + "parameters": { + "vault_id": vault_id, + "group_permissions_list": [ + o.model_dump(by_alias=True) + for o in group_permissions_list + ], + }, + }, + } + } + ) + + return None + + async def update_group_permissions( + self, group_permissions_list: List[GroupVaultAccess] + ) -> None: + response = await self.inner_client.invoke( + { + "invocation": { + "clientId": self.inner_client.client_id, + "parameters": { + "name": "VaultsUpdateGroupPermissions", + "parameters": { + "group_permissions_list": [ + o.model_dump(by_alias=True) + for o in group_permissions_list + ] + }, + }, + } + } + ) + + return None + + async def revoke_group_permissions(self, vault_id: str, group_id: str) -> None: + response = await self.inner_client.invoke( + { + "invocation": { + "clientId": self.inner_client.client_id, + "parameters": { + "name": "VaultsRevokeGroupPermissions", + "parameters": {"vault_id": vault_id, "group_id": group_id}, + }, + } + } + ) + + return None