Add bitwarden_item_id to bitwarden_login_credential (#1871)

Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
Shuchang Zheng 2025-03-03 11:45:50 -05:00 committed by GitHub
parent c7e6a5c84b
commit 8a1b0f3797
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 107 additions and 14 deletions

View file

@ -0,0 +1,37 @@
"""add bitwarden_item_id and make url_parameter_key optional in bitwarden_login_credential
Revision ID: 268dcc995513
Revises: a21b9f4f51d2
Create Date: 2025-03-03 16:42:11.868247+00:00
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "268dcc995513"
down_revision: Union[str, None] = "a21b9f4f51d2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("bitwarden_login_credential_parameters", sa.Column("bitwarden_item_id", sa.String(), nullable=True))
op.alter_column(
"bitwarden_login_credential_parameters", "url_parameter_key", existing_type=sa.VARCHAR(), nullable=True
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"bitwarden_login_credential_parameters", "url_parameter_key", existing_type=sa.VARCHAR(), nullable=False
)
op.drop_column("bitwarden_login_credential_parameters", "bitwarden_item_id")
# ### end Alembic commands ###

View file

@ -1586,10 +1586,11 @@ class AgentDB:
bitwarden_client_id_aws_secret_key: str,
bitwarden_client_secret_aws_secret_key: str,
bitwarden_master_password_aws_secret_key: str,
url_parameter_key: str,
key: str,
url_parameter_key: str | None = None,
description: str | None = None,
bitwarden_collection_id: str | None = None,
bitwarden_item_id: str | None = None,
) -> BitwardenLoginCredentialParameter:
async with self.Session() as session:
bitwarden_login_credential_parameter = BitwardenLoginCredentialParameterModel(
@ -1601,6 +1602,7 @@ class AgentDB:
key=key,
description=description,
bitwarden_collection_id=bitwarden_collection_id,
bitwarden_item_id=bitwarden_item_id,
)
session.add(bitwarden_login_credential_parameter)
await session.commit()

View file

@ -326,7 +326,8 @@ class BitwardenLoginCredentialParameterModel(Base):
bitwarden_client_secret_aws_secret_key = Column(String, nullable=False)
bitwarden_master_password_aws_secret_key = Column(String, nullable=False)
bitwarden_collection_id = Column(String, nullable=True, default=None)
url_parameter_key = Column(String, nullable=False)
bitwarden_item_id = Column(String, nullable=True, default=None)
url_parameter_key = Column(String, nullable=True, default=None)
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
modified_at = Column(
DateTime,

View file

@ -286,6 +286,7 @@ def convert_to_bitwarden_login_credential_parameter(
bitwarden_client_secret_aws_secret_key=bitwarden_login_credential_parameter_model.bitwarden_client_secret_aws_secret_key,
bitwarden_master_password_aws_secret_key=bitwarden_login_credential_parameter_model.bitwarden_master_password_aws_secret_key,
bitwarden_collection_id=bitwarden_login_credential_parameter_model.bitwarden_collection_id,
bitwarden_item_id=bitwarden_login_credential_parameter_model.bitwarden_item_id,
url_parameter_key=bitwarden_login_credential_parameter_model.url_parameter_key,
created_at=bitwarden_login_credential_parameter_model.created_at,
modified_at=bitwarden_login_credential_parameter_model.modified_at,

View file

@ -106,7 +106,7 @@ class BitwardenConstants(StrEnum):
URL = "BW_URL"
BW_COLLECTION_ID = "BW_COLLECTION_ID"
IDENTITY_KEY = "BW_IDENTITY_KEY"
ITEM_ID = "BW_ITEM_ID"
BW_ITEM_ID = "BW_ITEM_ID"
USERNAME = "BW_USERNAME"
PASSWORD = "BW_PASSWORD"
@ -190,8 +190,9 @@ class BitwardenService:
master_password: str,
bw_organization_id: str | None,
bw_collection_ids: list[str] | None,
url: str,
url: str | None = None,
collection_id: str | None = None,
item_id: str | None = None,
max_retries: int = settings.BITWARDEN_MAX_RETRIES,
timeout: int = settings.BITWARDEN_TIMEOUT_SECONDS,
) -> dict[str, str]:
@ -215,6 +216,7 @@ class BitwardenService:
bw_collection_ids=bw_collection_ids,
url=url,
collection_id=collection_id,
item_id=item_id,
timeout=timeout,
)
except BitwardenAccessDeniedError as e:
@ -271,8 +273,9 @@ class BitwardenService:
master_password: str,
bw_organization_id: str | None,
bw_collection_ids: list[str] | None,
url: str,
url: str | None = None,
collection_id: str | None = None,
item_id: str | None = None,
timeout: int = 60,
) -> dict[str, str]:
"""
@ -283,6 +286,26 @@ class BitwardenService:
await BitwardenService.sync()
session_key = await BitwardenService.unlock(master_password)
if item_id: # if item_id provided, get single item by item id
command = ["bw", "get", "item", item_id, "--session", session_key]
item_result = await BitwardenService.run_command(command)
if item_result.stderr:
raise BitwardenGetItemError(
f"Failed to get the bitwarden item {item_id}. Error: {item_result.stderr}"
)
try:
item = json.loads(item_result.stdout)
except json.JSONDecodeError:
raise BitwardenGetItemError(f"Failed to parse item JSON for item ID: {item_id}")
return {
BitwardenConstants.USERNAME: item["login"]["username"],
BitwardenConstants.PASSWORD: item["login"]["password"],
BitwardenConstants.TOTP: item["login"]["totp"],
}
elif not url:
# if item_id is not provided, we need a url to search for items
raise BitwardenGetItemError("No url or item ID provided")
# Extract the domain from the URL and search for items in Bitwarden with that domain
extract_url = tldextract.extract(url)
domain = extract_url.domain

View file

@ -165,6 +165,7 @@ class WorkflowRunContext:
bw_organization_id=self.secrets[BitwardenConstants.BW_ORGANIZATION_ID],
bw_collection_ids=self.secrets[BitwardenConstants.BW_COLLECTION_IDS],
collection_id=self.secrets[BitwardenConstants.BW_COLLECTION_ID],
item_id=self.secrets[BitwardenConstants.BW_ITEM_ID],
)
return secret_credentials
@ -241,11 +242,17 @@ class WorkflowRunContext:
LOG.error(f"Failed to get Bitwarden login credentials from AWS secrets. Error: {e}")
raise e
if self.has_parameter(parameter.url_parameter_key) and self.has_value(parameter.url_parameter_key):
if (
parameter.url_parameter_key
and self.has_parameter(parameter.url_parameter_key)
and self.has_value(parameter.url_parameter_key)
):
url = self.values[parameter.url_parameter_key]
elif parameter.url_parameter_key:
# If a key can't be found within the parameter values dict, assume it's a URL (and not a URL Parameter)
url = parameter.url_parameter_key
elif parameter.bitwarden_item_id:
url = None
else:
LOG.error(f"URL parameter {parameter.url_parameter_key} not found or has no value")
raise SkyvernException("URL parameter for Bitwarden login credentials not found or has no value")
@ -259,6 +266,13 @@ class WorkflowRunContext:
else:
collection_id = parameter.bitwarden_collection_id
item_id = None
if parameter.bitwarden_item_id:
if self.has_parameter(parameter.bitwarden_item_id) and self.has_value(parameter.bitwarden_item_id):
item_id = self.values[parameter.bitwarden_item_id]
else:
item_id = parameter.bitwarden_item_id
try:
secret_credentials = await BitwardenService.get_secret_value_from_url(
client_id,
@ -268,6 +282,7 @@ class WorkflowRunContext:
organization.bw_collection_ids,
url,
collection_id=collection_id,
item_id=item_id,
)
if secret_credentials:
self.secrets[BitwardenConstants.BW_ORGANIZATION_ID] = organization.bw_organization_id
@ -277,6 +292,7 @@ class WorkflowRunContext:
self.secrets[BitwardenConstants.CLIENT_ID] = client_id
self.secrets[BitwardenConstants.MASTER_PASSWORD] = master_password
self.secrets[BitwardenConstants.BW_COLLECTION_ID] = parameter.bitwarden_collection_id
self.secrets[BitwardenConstants.BW_ITEM_ID] = item_id
random_secret_id = self.generate_random_secret_id()
# username secret
@ -410,7 +426,7 @@ class WorkflowRunContext:
self.secrets[BitwardenConstants.CLIENT_ID] = client_id
self.secrets[BitwardenConstants.CLIENT_SECRET] = client_secret
self.secrets[BitwardenConstants.MASTER_PASSWORD] = master_password
self.secrets[BitwardenConstants.ITEM_ID] = item_id
self.secrets[BitwardenConstants.BW_ITEM_ID] = item_id
fields_to_obfuscate = {
BitwardenConstants.CREDIT_CARD_NUMBER: "card_number",

View file

@ -56,10 +56,12 @@ class BitwardenLoginCredentialParameter(Parameter):
bitwarden_client_secret_aws_secret_key: str
bitwarden_master_password_aws_secret_key: str
# url to request the login credentials from bitwarden
url_parameter_key: str
url_parameter_key: str | None = None
# bitwarden collection id to filter the login credentials from,
# if not provided, no filtering will be done
bitwarden_collection_id: str | None = None
# bitwarden item id to request the login credential
bitwarden_item_id: str | None = None
created_at: datetime
modified_at: datetime

View file

@ -37,10 +37,12 @@ class BitwardenLoginCredentialParameterYAML(ParameterYAML):
bitwarden_client_secret_aws_secret_key: str
bitwarden_master_password_aws_secret_key: str
# parameter key for the url to request the login credentials from bitwarden
url_parameter_key: str
url_parameter_key: str | None = None
# bitwarden collection id to filter the login credentials from,
# if not provided, no filtering will be done
bitwarden_collection_id: str | None = None
# bitwarden item id to request the login credential
bitwarden_item_id: str | None = None
class CredentialParameterYAML(ParameterYAML):

View file

@ -804,20 +804,22 @@ class WorkflowService:
bitwarden_client_id_aws_secret_key: str,
bitwarden_client_secret_aws_secret_key: str,
bitwarden_master_password_aws_secret_key: str,
url_parameter_key: str,
key: str,
url_parameter_key: str | None = None,
description: str | None = None,
bitwarden_collection_id: str | None = None,
bitwarden_item_id: str | None = None,
) -> Parameter:
return await app.DATABASE.create_bitwarden_login_credential_parameter(
workflow_id=workflow_id,
bitwarden_client_id_aws_secret_key=bitwarden_client_id_aws_secret_key,
bitwarden_client_secret_aws_secret_key=bitwarden_client_secret_aws_secret_key,
bitwarden_master_password_aws_secret_key=bitwarden_master_password_aws_secret_key,
url_parameter_key=url_parameter_key,
key=key,
url_parameter_key=url_parameter_key,
description=description,
bitwarden_collection_id=bitwarden_collection_id,
bitwarden_item_id=bitwarden_item_id,
)
async def create_credential_parameter(
@ -1397,11 +1399,17 @@ class WorkflowService:
credential_id=parameter.credential_id,
)
elif parameter.parameter_type == ParameterType.BITWARDEN_LOGIN_CREDENTIAL:
if not parameter.bitwarden_collection_id:
if not parameter.bitwarden_collection_id and not parameter.bitwarden_item_id:
raise WorkflowParameterMissingRequiredValue(
workflow_parameter_type=ParameterType.BITWARDEN_LOGIN_CREDENTIAL,
workflow_parameter_key=parameter.key,
required_value="bitwarden_collection_id",
required_value="bitwarden_collection_id or bitwarden_item_id",
)
if parameter.bitwarden_collection_id and not parameter.url_parameter_key:
raise WorkflowParameterMissingRequiredValue(
workflow_parameter_type=ParameterType.BITWARDEN_LOGIN_CREDENTIAL,
workflow_parameter_key=parameter.key,
required_value="url_parameter_key",
)
parameters[parameter.key] = await self.create_bitwarden_login_credential_parameter(
workflow_id=workflow.workflow_id,
@ -1412,6 +1420,7 @@ class WorkflowService:
key=parameter.key,
description=parameter.description,
bitwarden_collection_id=parameter.bitwarden_collection_id,
bitwarden_item_id=parameter.bitwarden_item_id,
)
elif parameter.parameter_type == ParameterType.BITWARDEN_SENSITIVE_INFORMATION:
parameters[parameter.key] = await self.create_bitwarden_sensitive_information_parameter(
@ -1434,7 +1443,7 @@ class WorkflowService:
bitwarden_master_password_aws_secret_key=parameter.bitwarden_master_password_aws_secret_key,
# TODO: remove "# type: ignore" after ensuring bitwarden_collection_id is always set
bitwarden_collection_id=parameter.bitwarden_collection_id, # type: ignore
bitwarden_item_id=parameter.bitwarden_item_id,
bitwarden_item_id=parameter.bitwarden_item_id, # type: ignore
key=parameter.key,
description=parameter.description,
)