Skip to content

Commit

Permalink
SM-874: Fix Python Integration (#229)
Browse files Browse the repository at this point in the history
## Type of change

<!-- (mark with an `X`) -->

```
- [x] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective

<!--Describe what the purpose of this PR is. For example: what bug
you're fixing or what new feature you're adding-->

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

<!--Explain the changes you've made to each file or major component.
This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->

- **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)
  • Loading branch information
coltonhurst authored Nov 27, 2023
1 parent af31957 commit 92a67b1
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 50 deletions.
1 change: 1 addition & 0 deletions crates/sdk-schemas/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
90 changes: 65 additions & 25 deletions languages/python/BitwardenClient/bitwarden_client.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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):
Expand All @@ -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)

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

Expand All @@ -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)
6 changes: 5 additions & 1 deletion languages/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
```bash
pip install setuptools_rust
```
- dateutil
```bash
pip install python-dateutil
```

# Installation

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

Expand Down
44 changes: 44 additions & 0 deletions languages/python/example.py
Original file line number Diff line number Diff line change
@@ -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))
24 changes: 0 additions & 24 deletions languages/python/login.py

This file was deleted.

0 comments on commit 92a67b1

Please sign in to comment.