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

View file

@ -326,7 +326,8 @@ class BitwardenLoginCredentialParameterModel(Base):
bitwarden_client_secret_aws_secret_key = Column(String, nullable=False) bitwarden_client_secret_aws_secret_key = Column(String, nullable=False)
bitwarden_master_password_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) 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) created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
modified_at = Column( modified_at = Column(
DateTime, 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_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_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_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, url_parameter_key=bitwarden_login_credential_parameter_model.url_parameter_key,
created_at=bitwarden_login_credential_parameter_model.created_at, created_at=bitwarden_login_credential_parameter_model.created_at,
modified_at=bitwarden_login_credential_parameter_model.modified_at, modified_at=bitwarden_login_credential_parameter_model.modified_at,

View file

@ -106,7 +106,7 @@ class BitwardenConstants(StrEnum):
URL = "BW_URL" URL = "BW_URL"
BW_COLLECTION_ID = "BW_COLLECTION_ID" BW_COLLECTION_ID = "BW_COLLECTION_ID"
IDENTITY_KEY = "BW_IDENTITY_KEY" IDENTITY_KEY = "BW_IDENTITY_KEY"
ITEM_ID = "BW_ITEM_ID" BW_ITEM_ID = "BW_ITEM_ID"
USERNAME = "BW_USERNAME" USERNAME = "BW_USERNAME"
PASSWORD = "BW_PASSWORD" PASSWORD = "BW_PASSWORD"
@ -190,8 +190,9 @@ class BitwardenService:
master_password: str, master_password: str,
bw_organization_id: str | None, bw_organization_id: str | None,
bw_collection_ids: list[str] | None, bw_collection_ids: list[str] | None,
url: str, url: str | None = None,
collection_id: str | None = None, collection_id: str | None = None,
item_id: str | None = None,
max_retries: int = settings.BITWARDEN_MAX_RETRIES, max_retries: int = settings.BITWARDEN_MAX_RETRIES,
timeout: int = settings.BITWARDEN_TIMEOUT_SECONDS, timeout: int = settings.BITWARDEN_TIMEOUT_SECONDS,
) -> dict[str, str]: ) -> dict[str, str]:
@ -215,6 +216,7 @@ class BitwardenService:
bw_collection_ids=bw_collection_ids, bw_collection_ids=bw_collection_ids,
url=url, url=url,
collection_id=collection_id, collection_id=collection_id,
item_id=item_id,
timeout=timeout, timeout=timeout,
) )
except BitwardenAccessDeniedError as e: except BitwardenAccessDeniedError as e:
@ -271,8 +273,9 @@ class BitwardenService:
master_password: str, master_password: str,
bw_organization_id: str | None, bw_organization_id: str | None,
bw_collection_ids: list[str] | None, bw_collection_ids: list[str] | None,
url: str, url: str | None = None,
collection_id: str | None = None, collection_id: str | None = None,
item_id: str | None = None,
timeout: int = 60, timeout: int = 60,
) -> dict[str, str]: ) -> dict[str, str]:
""" """
@ -283,6 +286,26 @@ class BitwardenService:
await BitwardenService.sync() await BitwardenService.sync()
session_key = await BitwardenService.unlock(master_password) 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 the domain from the URL and search for items in Bitwarden with that domain
extract_url = tldextract.extract(url) extract_url = tldextract.extract(url)
domain = extract_url.domain domain = extract_url.domain

View file

@ -165,6 +165,7 @@ class WorkflowRunContext:
bw_organization_id=self.secrets[BitwardenConstants.BW_ORGANIZATION_ID], bw_organization_id=self.secrets[BitwardenConstants.BW_ORGANIZATION_ID],
bw_collection_ids=self.secrets[BitwardenConstants.BW_COLLECTION_IDS], bw_collection_ids=self.secrets[BitwardenConstants.BW_COLLECTION_IDS],
collection_id=self.secrets[BitwardenConstants.BW_COLLECTION_ID], collection_id=self.secrets[BitwardenConstants.BW_COLLECTION_ID],
item_id=self.secrets[BitwardenConstants.BW_ITEM_ID],
) )
return secret_credentials return secret_credentials
@ -241,11 +242,17 @@ class WorkflowRunContext:
LOG.error(f"Failed to get Bitwarden login credentials from AWS secrets. Error: {e}") LOG.error(f"Failed to get Bitwarden login credentials from AWS secrets. Error: {e}")
raise 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] url = self.values[parameter.url_parameter_key]
elif 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) # 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 url = parameter.url_parameter_key
elif parameter.bitwarden_item_id:
url = None
else: else:
LOG.error(f"URL parameter {parameter.url_parameter_key} not found or has no value") 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") raise SkyvernException("URL parameter for Bitwarden login credentials not found or has no value")
@ -259,6 +266,13 @@ class WorkflowRunContext:
else: else:
collection_id = parameter.bitwarden_collection_id 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: try:
secret_credentials = await BitwardenService.get_secret_value_from_url( secret_credentials = await BitwardenService.get_secret_value_from_url(
client_id, client_id,
@ -268,6 +282,7 @@ class WorkflowRunContext:
organization.bw_collection_ids, organization.bw_collection_ids,
url, url,
collection_id=collection_id, collection_id=collection_id,
item_id=item_id,
) )
if secret_credentials: if secret_credentials:
self.secrets[BitwardenConstants.BW_ORGANIZATION_ID] = organization.bw_organization_id 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.CLIENT_ID] = client_id
self.secrets[BitwardenConstants.MASTER_PASSWORD] = master_password self.secrets[BitwardenConstants.MASTER_PASSWORD] = master_password
self.secrets[BitwardenConstants.BW_COLLECTION_ID] = parameter.bitwarden_collection_id 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() random_secret_id = self.generate_random_secret_id()
# username secret # username secret
@ -410,7 +426,7 @@ class WorkflowRunContext:
self.secrets[BitwardenConstants.CLIENT_ID] = client_id self.secrets[BitwardenConstants.CLIENT_ID] = client_id
self.secrets[BitwardenConstants.CLIENT_SECRET] = client_secret self.secrets[BitwardenConstants.CLIENT_SECRET] = client_secret
self.secrets[BitwardenConstants.MASTER_PASSWORD] = master_password 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 = { fields_to_obfuscate = {
BitwardenConstants.CREDIT_CARD_NUMBER: "card_number", BitwardenConstants.CREDIT_CARD_NUMBER: "card_number",

View file

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

View file

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

View file

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