From 92a67b194e59ba445531abbdd76971c250d56f3e Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 27 Nov 2023 11:00:44 -0500 Subject: [PATCH] SM-874: Fix Python Integration (#229) ## Type of change ``` - [x] Bug fix - [ ] New feature development - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Build/deploy pipeline (DevOps) - [ ] Other ``` ## Objective The Python integration is out of date and is not currently working. This PR fixes the Python integration by adding the ability to log in via access token, which is currently the supported way to interact with the Bitwarden Secrets Manager SDK. This PR also adds a `ProjectsClient` to `bitwarden_client.py` for easy use, and updates the error handling on Project or Secret deletes in the SDK. ## Code changes - **crates/sdk-schemas/src/main.rs:** We need the `AccessTokenLoginResponse` struct to be generated so access token auth is available to integrations - **languages/python/BitwardenClient/bitwarden_client.py:** - Import the following from `.schemas`: - `AccessTokenLoginRequest` - `AccessTokenLoginResponse` - `ResponseForAccessTokenLoginResponse` - Define a new function `access_token_login` to support authenticating with an access token - Add the `project_ids` parameter to the `create` and `update` functions for `SecretsClient` - Add a `ProjectsClient` class and `projects()` to the `BitwardenClient` - Removed the password login methods, as they are not supported - Removed the imports that are not needed, and add one that are (like `sys`, etc.) - **languages/python/README.md:** Update the readme instructions, give more examples - **languages/python/login.py:** Renamed `login.py` to `example.py` - **languages/python/example.py:** Update the example to showcase auth with an access token ## Before you submit - Please add **unit tests** where it makes sense to do so (encouraged but not required) --- crates/sdk-schemas/src/main.rs | 1 + .../BitwardenClient/bitwarden_client.py | 90 +++++++++++++------ languages/python/README.md | 6 +- languages/python/example.py | 44 +++++++++ languages/python/login.py | 24 ----- 5 files changed, 115 insertions(+), 50 deletions(-) create mode 100644 languages/python/example.py delete mode 100644 languages/python/login.py diff --git a/crates/sdk-schemas/src/main.rs b/crates/sdk-schemas/src/main.rs index 9d7e52a47..ef7a2db83 100644 --- a/crates/sdk-schemas/src/main.rs +++ b/crates/sdk-schemas/src/main.rs @@ -99,6 +99,7 @@ fn main() -> Result<()> { write_schema_for_response! { bitwarden::auth::login::ApiKeyLoginResponse, bitwarden::auth::login::PasswordLoginResponse, + bitwarden::auth::login::AccessTokenLoginResponse, bitwarden::secrets_manager::secrets::SecretIdentifiersResponse, bitwarden::secrets_manager::secrets::SecretResponse, bitwarden::secrets_manager::secrets::SecretsResponse, diff --git a/languages/python/BitwardenClient/bitwarden_client.py b/languages/python/BitwardenClient/bitwarden_client.py index 6a0a77ed8..cb669a40e 100644 --- a/languages/python/BitwardenClient/bitwarden_client.py +++ b/languages/python/BitwardenClient/bitwarden_client.py @@ -1,8 +1,8 @@ import json -from typing import Any, List +from typing import Any, List, Optional +from uuid import UUID import bitwarden_py -from .schemas import ClientSettings, Command, PasswordLoginRequest, PasswordLoginResponse, ResponseForPasswordLoginResponse, ResponseForSecretIdentifiersResponse, ResponseForSecretResponse, ResponseForSecretsDeleteResponse, ResponseForSyncResponse, ResponseForUserAPIKeyResponse, SecretCreateRequest, SecretGetRequest, SecretIdentifiersRequest, SecretIdentifiersResponse, SecretPutRequest, SecretResponse, SecretVerificationRequest, SecretsCommand, SecretsDeleteRequest, SecretsDeleteResponse, SyncRequest, SyncResponse, UserAPIKeyResponse - +from .schemas import ClientSettings, Command, ResponseForSecretIdentifiersResponse, ResponseForSecretResponse, ResponseForSecretsDeleteResponse, SecretCreateRequest, SecretGetRequest, SecretIdentifiersRequest, SecretIdentifiersResponse, SecretPutRequest, SecretResponse, SecretsCommand, SecretsDeleteRequest, SecretsDeleteResponse, AccessTokenLoginRequest, AccessTokenLoginResponse, ResponseForAccessTokenLoginResponse, ResponseForProjectResponse, ProjectsCommand, ProjectCreateRequest, ProjectGetRequest, ProjectPutRequest, ProjectsListRequest, ResponseForProjectsResponse, ResponseForProjectsDeleteResponse, ProjectsDeleteRequest class BitwardenClient: def __init__(self, settings: ClientSettings = None): @@ -12,32 +12,25 @@ def __init__(self, settings: ClientSettings = None): settings_json = json.dumps(settings.to_dict()) self.inner = bitwarden_py.BitwardenClient(settings_json) - def password_login(self, email: str, password: str) -> ResponseForPasswordLoginResponse: - result = self._run_command( - Command(password_login=PasswordLoginRequest(email, password)) - ) - return ResponseForPasswordLoginResponse.from_dict(result) - - def get_user_api_key(self, secret: str, is_otp: bool = False) -> ResponseForUserAPIKeyResponse: - result = self._run_command( - Command(get_user_api_key=SecretVerificationRequest( - secret if not is_otp else None, secret if is_otp else None)) + def access_token_login(self, access_token: str): + self._run_command( + Command(access_token_login=AccessTokenLoginRequest(access_token)) ) - return ResponseForUserAPIKeyResponse.from_dict(result) - - def sync(self, exclude_subdomains: bool = False) -> ResponseForSyncResponse: - result = self._run_command( - Command(sync=SyncRequest(exclude_subdomains)) - ) - return ResponseForSyncResponse.from_dict(result) def secrets(self): return SecretsClient(self) + def projects(self): + return ProjectsClient(self) + def _run_command(self, command: Command) -> Any: response_json = self.inner.run_command(json.dumps(command.to_dict())) - return json.loads(response_json) + response = json.loads(response_json) + if response["success"] == False: + raise Exception(response["errorMessage"]) + + return response class SecretsClient: def __init__(self, client: BitwardenClient): @@ -52,10 +45,12 @@ def get(self, id: str) -> ResponseForSecretResponse: def create(self, key: str, note: str, organization_id: str, - value: str) -> ResponseForSecretResponse: + value: str, + project_ids: Optional[List[UUID]] = None + ) -> ResponseForSecretResponse: result = self.client._run_command( Command(secrets=SecretsCommand( - create=SecretCreateRequest(key, note, organization_id, value))) + create=SecretCreateRequest(key, note, organization_id, value, project_ids))) ) return ResponseForSecretResponse.from_dict(result) @@ -70,10 +65,12 @@ def update(self, id: str, key: str, note: str, organization_id: str, - value: str) -> ResponseForSecretResponse: + value: str, + project_ids: Optional[List[UUID]] = None + ) -> ResponseForSecretResponse: result = self.client._run_command( Command(secrets=SecretsCommand(update=SecretPutRequest( - id, key, note, organization_id, value))) + id, key, note, organization_id, value, project_ids))) ) return ResponseForSecretResponse.from_dict(result) @@ -82,3 +79,46 @@ def delete(self, ids: List[str]) -> ResponseForSecretsDeleteResponse: Command(secrets=SecretsCommand(delete=SecretsDeleteRequest(ids))) ) return ResponseForSecretsDeleteResponse.from_dict(result) + +class ProjectsClient: + def __init__(self, client: BitwardenClient): + self.client = client + + def get(self, id: str) -> ResponseForProjectResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand(get=ProjectGetRequest(id))) + ) + return ResponseForProjectResponse.from_dict(result) + + def create(self, + name: str, + organization_id: str, + ) -> ResponseForProjectResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand( + create=ProjectCreateRequest(name, organization_id))) + ) + return ResponseForProjectResponse.from_dict(result) + + def list(self, organization_id: str) -> ResponseForProjectsResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand( + list=ProjectsListRequest(organization_id))) + ) + return ResponseForProjectsResponse.from_dict(result) + + def update(self, id: str, + name: str, + organization_id: str, + ) -> ResponseForProjectResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand(update=ProjectPutRequest( + id, name, organization_id))) + ) + return ResponseForProjectResponse.from_dict(result) + + def delete(self, ids: List[str]) -> ResponseForProjectsDeleteResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand(delete=ProjectsDeleteRequest(ids))) + ) + return ResponseForProjectsDeleteResponse.from_dict(result) diff --git a/languages/python/README.md b/languages/python/README.md index 03a4ce572..871be274f 100644 --- a/languages/python/README.md +++ b/languages/python/README.md @@ -9,6 +9,10 @@ ```bash pip install setuptools_rust ``` +- dateutil + ```bash + pip install python-dateutil + ``` # Installation @@ -18,7 +22,7 @@ From the `languages/python/` directory, python3 ./setup.py develop ``` -Move the the resulting `.so` file to `bitwarden_py.so`, if it isn't already there. +Rename the the resulting `.so` file to `bitwarden_py.so`, if it isn't already there. # Run diff --git a/languages/python/example.py b/languages/python/example.py new file mode 100644 index 000000000..2ee1cb280 --- /dev/null +++ b/languages/python/example.py @@ -0,0 +1,44 @@ +import json +import logging +import sys +from BitwardenClient.bitwarden_client import BitwardenClient +from BitwardenClient.schemas import client_settings_from_dict, DeviceType + +# Create the BitwardenClient, which is used to interact with the SDK +client = BitwardenClient(client_settings_from_dict({ + "apiUrl": "http://localhost:4000", + "deviceType": DeviceType.SDK, + "identityUrl": "http://localhost:33656", + "userAgent": "Python", +})) + +# Add some logging & set the org id +logging.basicConfig(level=logging.DEBUG) +organization_id = "org_id_here" + +# Attempt to authenticate with the Secrets Manager Access Token +client.access_token_login("access_token_here") + +# -- Example Project Commands -- + +project = client.projects().create("ProjectName", organization_id) +project2 = client.projects().create("Project - Don't Delete Me!", organization_id) +updated_project = client.projects().update(project.data.id, "Cool New Project Name", organization_id) +get_that_project = client.projects().get(project.data.id) + +input("Press Enter to delete the project...") +client.projects().delete([project.data.id]) + +print(client.projects().list(organization_id)) + +# -- Example Secret Commands -- + +secret = client.secrets().create("TEST_SECRET", "This is a test secret", organization_id, "Secret1234!", [project2.data.id]) +secret2 = client.secrets().create("Secret - Don't Delete Me!", "This is a test secret that will stay", organization_id, "Secret1234!", [project2.data.id]) +secret_updated = client.secrets().update(secret.data.id, "TEST_SECRET_UPDATED", "This as an updated test secret", organization_id, "Secret1234!_updated", [project2.data.id]) +secret_retrieved = client.secrets().get(secret.data.id) + +input("Press Enter to delete the secret...") +client.secrets().delete([secret.data.id]) + +print(client.secrets().list(organization_id)) diff --git a/languages/python/login.py b/languages/python/login.py deleted file mode 100644 index 9b756a005..000000000 --- a/languages/python/login.py +++ /dev/null @@ -1,24 +0,0 @@ -import json -import logging -from BitwardenClient.bitwarden_client import BitwardenClient -from BitwardenClient.schemas import client_settings_from_dict - -client = BitwardenClient(client_settings_from_dict({ - "api_url": "http://localhost:4000", - "identity_url": "http://localhost:33656", - "user_agent": "Python", -})) - -logging.basicConfig(level=logging.DEBUG) - -result = client.password_login("test@bitwarden.com", "asdfasdf") -print(result) -print(client.get_user_api_key("asdfasdf")) - -sync = client.sync() - -secret = client.secrets().create("TEST_SECRET", "This is a test secret", - sync.data.profile.organizations[0].id, "Secret1234!") -print(secret) - -client.secrets().delete([secret.data.id])