diff --git a/alembic/versions/2024_10_03_2301-a575628e1965_create_bitwarden_credit_card_data_.py b/alembic/versions/2024_10_03_2301-a575628e1965_create_bitwarden_credit_card_data_.py new file mode 100644 index 00000000..2f7a5407 --- /dev/null +++ b/alembic/versions/2024_10_03_2301-a575628e1965_create_bitwarden_credit_card_data_.py @@ -0,0 +1,69 @@ +"""create bitwarden_credit_card_data_parameters table + +Revision ID: a575628e1965 +Revises: 6c90d565076b +Create Date: 2024-10-03 23:01:05.810580+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "a575628e1965" +down_revision: Union[str, None] = "6c90d565076b" +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.create_table( + "bitwarden_credit_card_data_parameters", + sa.Column("bitwarden_credit_card_data_parameter_id", sa.String(), nullable=False), + sa.Column("workflow_id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("bitwarden_client_id_aws_secret_key", sa.String(), nullable=False), + sa.Column("bitwarden_client_secret_aws_secret_key", sa.String(), nullable=False), + sa.Column("bitwarden_master_password_aws_secret_key", sa.String(), nullable=False), + sa.Column("bitwarden_collection_id", sa.String(), nullable=False), + sa.Column("bitwarden_item_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("modified_at", sa.DateTime(), nullable=False), + sa.Column("deleted_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["workflow_id"], + ["workflows.workflow_id"], + ), + sa.PrimaryKeyConstraint("bitwarden_credit_card_data_parameter_id"), + ) + op.create_index( + op.f("ix_bitwarden_credit_card_data_parameters_bitwarden_credit_card_data_parameter_id"), + "bitwarden_credit_card_data_parameters", + ["bitwarden_credit_card_data_parameter_id"], + unique=False, + ) + op.create_index( + op.f("ix_bitwarden_credit_card_data_parameters_workflow_id"), + "bitwarden_credit_card_data_parameters", + ["workflow_id"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + op.f("ix_bitwarden_credit_card_data_parameters_workflow_id"), table_name="bitwarden_credit_card_data_parameters" + ) + op.drop_index( + op.f("ix_bitwarden_credit_card_data_parameters_bitwarden_credit_card_data_parameter_id"), + table_name="bitwarden_credit_card_data_parameters", + ) + op.drop_table("bitwarden_credit_card_data_parameters") + # ### end Alembic commands ### diff --git a/skyvern/exceptions.py b/skyvern/exceptions.py index df5a9c85..e7e4bd4c 100644 --- a/skyvern/exceptions.py +++ b/skyvern/exceptions.py @@ -281,9 +281,9 @@ class BitwardenSyncError(BitwardenBaseError): class BitwardenAccessDeniedError(BitwardenBaseError): def __init__(self) -> None: super().__init__( - "Current organization does not have access to the specified Bitwarden collection. \ - Contact Skyvern support to enable access. This is a security layer on top of Bitwarden, \ - Skyvern team needs to let your Skyvern account access the Bitwarden collection." + "Current organization does not have access to the specified Bitwarden collection. " + "Contact Skyvern support to enable access. This is a security layer on top of Bitwarden, " + "Skyvern team needs to let your Skyvern account access the Bitwarden collection." ) diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index 62c53000..2053fc0a 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -15,6 +15,7 @@ from skyvern.forge.sdk.db.exceptions import NotFoundError from skyvern.forge.sdk.db.models import ( ArtifactModel, AWSSecretParameterModel, + BitwardenCreditCardDataParameterModel, BitwardenLoginCredentialParameterModel, BitwardenSensitiveInformationParameterModel, OrganizationAuthTokenModel, @@ -53,6 +54,7 @@ from skyvern.forge.sdk.schemas.tasks import ProxyLocation, Task, TaskStatus from skyvern.forge.sdk.schemas.totp_codes import TOTPCode from skyvern.forge.sdk.workflow.models.parameter import ( AWSSecretParameter, + BitwardenCreditCardDataParameter, BitwardenLoginCredentialParameter, BitwardenSensitiveInformationParameter, OutputParameter, @@ -1218,6 +1220,33 @@ class AgentDB: await session.refresh(bitwarden_sensitive_information_parameter) return convert_to_bitwarden_sensitive_information_parameter(bitwarden_sensitive_information_parameter) + async def create_bitwarden_credit_card_data_parameter( + self, + workflow_id: str, + bitwarden_client_id_aws_secret_key: str, + bitwarden_client_secret_aws_secret_key: str, + bitwarden_master_password_aws_secret_key: str, + bitwarden_collection_id: str, + bitwarden_item_id: str, + key: str, + description: str | None = None, + ) -> BitwardenCreditCardDataParameter: + async with self.Session() as session: + bitwarden_credit_card_data_parameter = BitwardenCreditCardDataParameterModel( + 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, + bitwarden_collection_id=bitwarden_collection_id, + bitwarden_item_id=bitwarden_item_id, + key=key, + description=description, + ) + session.add(bitwarden_credit_card_data_parameter) + await session.commit() + await session.refresh(bitwarden_credit_card_data_parameter) + return BitwardenCreditCardDataParameter.model_validate(bitwarden_credit_card_data_parameter) + async def create_output_parameter( self, workflow_id: str, diff --git a/skyvern/forge/sdk/db/id.py b/skyvern/forge/sdk/db/id.py index ade5bc21..c3c13fe4 100644 --- a/skyvern/forge/sdk/db/id.py +++ b/skyvern/forge/sdk/db/id.py @@ -41,6 +41,7 @@ AWS_SECRET_PARAMETER_PREFIX = "asp" OUTPUT_PARAMETER_PREFIX = "op" BITWARDEN_LOGIN_CREDENTIAL_PARAMETER_PREFIX = "blc" BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX = "bsi" +BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX = "bccd" TASK_GENERATION_PREFIX = "tg" @@ -84,6 +85,11 @@ def generate_bitwarden_sensitive_information_parameter_id() -> str: return f"{BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX}_{int_id}" +def generate_bitwarden_credit_card_data_parameter_id() -> str: + int_id = generate_id() + return f"{BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX}_{int_id}" + + def generate_organization_auth_token_id() -> str: int_id = generate_id() return f"{ORGANIZATION_AUTH_TOKEN_PREFIX}_{int_id}" diff --git a/skyvern/forge/sdk/db/models.py b/skyvern/forge/sdk/db/models.py index e4ac168c..9c857905 100644 --- a/skyvern/forge/sdk/db/models.py +++ b/skyvern/forge/sdk/db/models.py @@ -21,6 +21,7 @@ from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType from skyvern.forge.sdk.db.id import ( generate_artifact_id, generate_aws_secret_parameter_id, + generate_bitwarden_credit_card_data_parameter_id, generate_bitwarden_login_credential_parameter_id, generate_bitwarden_sensitive_information_parameter_id, generate_org_id, @@ -333,6 +334,28 @@ class BitwardenSensitiveInformationParameterModel(Base): deleted_at = Column(DateTime, nullable=True) +class BitwardenCreditCardDataParameterModel(Base): + __tablename__ = "bitwarden_credit_card_data_parameters" + + bitwarden_credit_card_data_parameter_id = Column( + String, + primary_key=True, + index=True, + default=generate_bitwarden_credit_card_data_parameter_id, + ) + workflow_id = Column(String, ForeignKey("workflows.workflow_id"), index=True, nullable=False) + key = Column(String, nullable=False) + description = Column(String, nullable=True) + bitwarden_client_id_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_collection_id = Column(String, nullable=False) + bitwarden_item_id = Column(String, nullable=False) + created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) + modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False) + deleted_at = Column(DateTime, nullable=True) + + class WorkflowRunParameterModel(Base): __tablename__ = "workflow_run_parameters" diff --git a/skyvern/forge/sdk/routes/agent_protocol.py b/skyvern/forge/sdk/routes/agent_protocol.py index e16dafb9..2ecf8d7c 100644 --- a/skyvern/forge/sdk/routes/agent_protocol.py +++ b/skyvern/forge/sdk/routes/agent_protocol.py @@ -668,7 +668,7 @@ async def create_workflow( try: workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) return await app.WORKFLOW_SERVICE.create_workflow_from_request( - organization_id=current_org.organization_id, request=workflow_create_request + organization=current_org, request=workflow_create_request ) except Exception as e: LOG.error("Failed to create workflow", exc_info=True) @@ -712,7 +712,7 @@ async def update_workflow( try: workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) return await app.WORKFLOW_SERVICE.create_workflow_from_request( - organization_id=current_org.organization_id, + organization=current_org, request=workflow_create_request, workflow_permanent_id=workflow_permanent_id, ) diff --git a/skyvern/forge/sdk/services/bitwarden.py b/skyvern/forge/sdk/services/bitwarden.py index f2ef1294..07bba0e5 100644 --- a/skyvern/forge/sdk/services/bitwarden.py +++ b/skyvern/forge/sdk/services/bitwarden.py @@ -39,11 +39,19 @@ class BitwardenConstants(StrEnum): URL = "BW_URL" BW_COLLECTION_ID = "BW_COLLECTION_ID" IDENTITY_KEY = "BW_IDENTITY_KEY" + ITEM_ID = "BW_ITEM_ID" USERNAME = "BW_USERNAME" PASSWORD = "BW_PASSWORD" TOTP = "BW_TOTP" + CREDIT_CARD_HOLDER_NAME = "BW_CREDIT_CARD_HOLDER_NAME" + CREDIT_CARD_NUMBER = "BW_CREDIT_CARD_NUMBER" + CREDIT_CARD_EXPIRATION_MONTH = "BW_CREDIT_CARD_EXPIRATION_MONTH" + CREDIT_CARD_EXPIRATION_YEAR = "BW_CREDIT_CARD_EXPIRATION_YEAR" + CREDIT_CARD_CVV = "BW_CREDIT_CARD_CVV" + CREDIT_CARD_BRAND = "BW_CREDIT_CARD_BRAND" + class BitwardenService: @staticmethod @@ -94,6 +102,8 @@ class BitwardenService: """ Get the secret value from the Bitwarden CLI. """ + if not bw_organization_id and bw_collection_ids and collection_id not in bw_collection_ids: + raise BitwardenAccessDeniedError() try: async with asyncio.timeout(timeout): return await BitwardenService._get_secret_value_from_url( @@ -105,6 +115,8 @@ class BitwardenService: url=url, collection_id=collection_id, ) + except BitwardenAccessDeniedError as e: + raise e except Exception as e: if remaining_retries <= 0: raise BitwardenListItemsError( @@ -140,8 +152,6 @@ class BitwardenService: """ Get the secret value from the Bitwarden CLI. """ - if not bw_organization_id and bw_collection_ids and collection_id not in bw_collection_ids: - raise BitwardenAccessDeniedError() try: BitwardenService.login(client_id, client_secret) BitwardenService.sync() @@ -255,6 +265,8 @@ class BitwardenService: """ Get the secret value from the Bitwarden CLI. """ + if not bw_organization_id and bw_collection_ids and collection_id not in bw_collection_ids: + raise BitwardenAccessDeniedError() try: async with asyncio.timeout(timeout): return await BitwardenService._get_sensitive_information_from_identity( @@ -267,6 +279,8 @@ class BitwardenService: identity_key=identity_key, identity_fields=identity_fields, ) + except BitwardenAccessDeniedError as e: + raise e except Exception as e: if remaining_retries <= 0: raise BitwardenListItemsError( @@ -304,8 +318,6 @@ class BitwardenService: """ Get the sensitive information from the Bitwarden CLI. """ - if not bw_organization_id and bw_collection_ids and collection_id not in bw_collection_ids: - raise BitwardenAccessDeniedError() try: BitwardenService.login(client_id, client_secret) BitwardenService.sync() @@ -438,3 +450,123 @@ class BitwardenService: logout_result = BitwardenService.run_command(logout_command) if logout_result.stderr and "You are not logged in." not in logout_result.stderr: raise BitwardenLogoutError(logout_result.stderr) + + @staticmethod + async def _get_credit_card_data( + client_id: str, + client_secret: str, + master_password: str, + bw_organization_id: str | None, + bw_collection_ids: list[str] | None, + collection_id: str, + item_id: str, + ) -> dict[str, str]: + """ + Get the credit card data from the Bitwarden CLI. + """ + try: + BitwardenService.login(client_id, client_secret) + BitwardenService.sync() + session_key = BitwardenService.unlock(master_password) + + # Step 3: Get the item + get_command = [ + "bw", + "get", + "item", + item_id, + "--session", + session_key, + ] + item_result = BitwardenService.run_command(get_command) + + # Parse the item and extract credit card data + try: + item = json.loads(item_result.stdout) + except json.JSONDecodeError: + raise BitwardenListItemsError(f"Failed to parse item JSON for item ID: {item_id}") + + if not item: + raise BitwardenListItemsError(f"No item found in Bitwarden for item ID: {item_id}") + + # Check if the bw_organization_id matches + if bw_organization_id: + item_organization_id = item.get("organizationId") + if item_organization_id != bw_organization_id: + raise BitwardenAccessDeniedError() + + if bw_collection_ids: + item_collection_ids = item.get("collectionIds") + if item_collection_ids and collection_id not in bw_collection_ids: + raise BitwardenAccessDeniedError() + + # Check if the item is a credit card + # https://bitwarden.com/help/cli/#create lists the type of the credit card items as 3 + if item["type"] != 3: + raise BitwardenListItemsError(f"Item with ID: {item_id} is not a credit card type") + + credit_card_data = item["card"] + + mapped_credit_card_data: dict[str, str] = { + BitwardenConstants.CREDIT_CARD_HOLDER_NAME: credit_card_data["cardholderName"], + BitwardenConstants.CREDIT_CARD_NUMBER: credit_card_data["number"], + BitwardenConstants.CREDIT_CARD_EXPIRATION_MONTH: credit_card_data["expMonth"], + BitwardenConstants.CREDIT_CARD_EXPIRATION_YEAR: credit_card_data["expYear"], + BitwardenConstants.CREDIT_CARD_CVV: credit_card_data["code"], + BitwardenConstants.CREDIT_CARD_BRAND: credit_card_data["brand"], + } + + return mapped_credit_card_data + finally: + # Step 4: Log out + BitwardenService.logout() + + @staticmethod + async def get_credit_card_data( + client_id: str, + client_secret: str, + master_password: str, + bw_organization_id: str | None, + bw_collection_ids: list[str] | None, + collection_id: str, + item_id: str, + remaining_retries: int = settings.BITWARDEN_MAX_RETRIES, + fail_reasons: list[str] = [], + ) -> dict[str, str]: + """ + Get the credit card data from the Bitwarden CLI. + """ + if not bw_organization_id and not bw_collection_ids: + raise BitwardenAccessDeniedError() + try: + async with asyncio.timeout(settings.BITWARDEN_TIMEOUT_SECONDS): + return await BitwardenService._get_credit_card_data( + client_id=client_id, + client_secret=client_secret, + master_password=master_password, + bw_organization_id=bw_organization_id, + bw_collection_ids=bw_collection_ids, + collection_id=collection_id, + item_id=item_id, + ) + except BitwardenAccessDeniedError as e: + raise e + except Exception as e: + if remaining_retries <= 0: + raise BitwardenListItemsError( + f"Bitwarden CLI failed after all retry attempts. Fail reasons: {fail_reasons}" + ) + + remaining_retries -= 1 + LOG.info("Retrying to get credit card data from Bitwarden", remaining_retries=remaining_retries) + return await BitwardenService.get_credit_card_data( + client_id=client_id, + client_secret=client_secret, + master_password=master_password, + bw_organization_id=bw_organization_id, + bw_collection_ids=bw_collection_ids, + collection_id=collection_id, + item_id=item_id, + remaining_retries=remaining_retries, + fail_reasons=fail_reasons + [f"{type(e).__name__}: {str(e)}"], + ) diff --git a/skyvern/forge/sdk/workflow/context_manager.py b/skyvern/forge/sdk/workflow/context_manager.py index 05e5f99d..e2246717 100644 --- a/skyvern/forge/sdk/workflow/context_manager.py +++ b/skyvern/forge/sdk/workflow/context_manager.py @@ -203,7 +203,6 @@ class WorkflowRunContext: except BitwardenBaseError as e: LOG.error(f"Failed to get secret from Bitwarden. Error: {e}") raise e - # TODO (kerem): Implement this elif parameter.parameter_type == ParameterType.BITWARDEN_SENSITIVE_INFORMATION: try: # Get the Bitwarden login credentials from AWS secrets @@ -256,6 +255,73 @@ class WorkflowRunContext: except BitwardenBaseError as e: LOG.error(f"Failed to get sensitive information from Bitwarden. Error: {e}") raise e + elif parameter.parameter_type == ParameterType.BITWARDEN_CREDIT_CARD_DATA: + try: + # Get the Bitwarden login credentials from AWS secrets + client_id = await aws_client.get_secret(parameter.bitwarden_client_id_aws_secret_key) + client_secret = await aws_client.get_secret(parameter.bitwarden_client_secret_aws_secret_key) + master_password = await aws_client.get_secret(parameter.bitwarden_master_password_aws_secret_key) + except Exception as e: + LOG.error(f"Failed to get Bitwarden login credentials from AWS secrets. Error: {e}") + raise e + + 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 + + if self.has_parameter(parameter.bitwarden_collection_id) and self.has_value( + parameter.bitwarden_collection_id + ): + collection_id = self.values[parameter.bitwarden_collection_id] + else: + collection_id = parameter.bitwarden_collection_id + + try: + credit_card_data = await BitwardenService.get_credit_card_data( + client_id, + client_secret, + master_password, + organization.bw_organization_id, + organization.bw_collection_ids, + collection_id, + item_id, + ) + if not credit_card_data: + raise ValueError("Credit card data not found in Bitwarden") + + 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 + + fields_to_obfuscate = { + BitwardenConstants.CREDIT_CARD_NUMBER: "card_number", + BitwardenConstants.CREDIT_CARD_CVV: "card_cvv", + } + + pass_through_fields = { + BitwardenConstants.CREDIT_CARD_HOLDER_NAME: "card_holder_name", + BitwardenConstants.CREDIT_CARD_EXPIRATION_MONTH: "card_exp_month", + BitwardenConstants.CREDIT_CARD_EXPIRATION_YEAR: "card_exp_year", + BitwardenConstants.CREDIT_CARD_BRAND: "card_brand", + } + + parameter_value: dict[str, Any] = { + field_name: credit_card_data[field_key] for field_key, field_name in pass_through_fields.items() + } + + for data_key, secret_suffix in fields_to_obfuscate.items(): + random_secret_id = self.generate_random_secret_id() + secret_id = f"{random_secret_id}_{secret_suffix}" + self.secrets[secret_id] = credit_card_data[data_key] + parameter_value[secret_suffix] = secret_id + + self.values[parameter.key] = parameter_value + + except BitwardenBaseError as e: + LOG.error(f"Failed to get credit card data from Bitwarden. Error: {e}") + raise e elif isinstance(parameter, ContextParameter): if isinstance(parameter.source, WorkflowParameter): # TODO (kerem): set this while initializing the context manager diff --git a/skyvern/forge/sdk/workflow/models/parameter.py b/skyvern/forge/sdk/workflow/models/parameter.py index 4740d247..1ebd943c 100644 --- a/skyvern/forge/sdk/workflow/models/parameter.py +++ b/skyvern/forge/sdk/workflow/models/parameter.py @@ -4,7 +4,7 @@ from datetime import datetime from enum import StrEnum from typing import Annotated, Literal, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class ParameterType(StrEnum): @@ -13,6 +13,7 @@ class ParameterType(StrEnum): AWS_SECRET = "aws_secret" BITWARDEN_LOGIN_CREDENTIAL = "bitwarden_login_credential" BITWARDEN_SENSITIVE_INFORMATION = "bitwarden_sensitive_information" + BITWARDEN_CREDIT_CARD_DATA = "bitwarden_credit_card_data" OUTPUT = "output" @@ -86,6 +87,25 @@ class BitwardenSensitiveInformationParameter(Parameter): deleted_at: datetime | None = None +class BitwardenCreditCardDataParameter(Parameter): + model_config = ConfigDict(from_attributes=True) + parameter_type: Literal[ParameterType.BITWARDEN_CREDIT_CARD_DATA] = ParameterType.BITWARDEN_CREDIT_CARD_DATA + # parameter fields + bitwarden_credit_card_data_parameter_id: str + workflow_id: str + # bitwarden cli required fields + bitwarden_client_id_aws_secret_key: str + bitwarden_client_secret_aws_secret_key: str + bitwarden_master_password_aws_secret_key: str + # bitwarden ids for the credit card item + bitwarden_collection_id: str + bitwarden_item_id: str + + created_at: datetime + modified_at: datetime + deleted_at: datetime | None = None + + class WorkflowParameterType(StrEnum): STRING = "string" INTEGER = "integer" @@ -150,6 +170,7 @@ ParameterSubclasses = Union[ AWSSecretParameter, BitwardenLoginCredentialParameter, BitwardenSensitiveInformationParameter, + BitwardenCreditCardDataParameter, OutputParameter, ] PARAMETER_TYPE = Annotated[ParameterSubclasses, Field(discriminator="parameter_type")] diff --git a/skyvern/forge/sdk/workflow/models/yaml.py b/skyvern/forge/sdk/workflow/models/yaml.py index 5457b688..215bab95 100644 --- a/skyvern/forge/sdk/workflow/models/yaml.py +++ b/skyvern/forge/sdk/workflow/models/yaml.py @@ -61,6 +61,21 @@ class BitwardenSensitiveInformationParameterYAML(ParameterYAML): bitwarden_identity_fields: list[str] +class BitwardenCreditCardDataParameterYAML(ParameterYAML): + # There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error: + # Parameter 1 of Literal[...] cannot be of type "Any" + # This pattern already works in block.py but since the ParameterType is not defined in this file, mypy is not able + # to infer the type of the parameter_type attribute. + parameter_type: Literal[ParameterType.BITWARDEN_CREDIT_CARD_DATA] = ParameterType.BITWARDEN_CREDIT_CARD_DATA # type: ignore + # bitwarden cli required fields + bitwarden_client_id_aws_secret_key: str + bitwarden_client_secret_aws_secret_key: str + bitwarden_master_password_aws_secret_key: str + # bitwarden ids for the credit card item + bitwarden_collection_id: str + bitwarden_item_id: str + + class WorkflowParameterYAML(ParameterYAML): # There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error: # Parameter 1 of Literal[...] cannot be of type "Any" @@ -196,6 +211,7 @@ PARAMETER_YAML_SUBCLASSES = ( AWSSecretParameterYAML | BitwardenLoginCredentialParameterYAML | BitwardenSensitiveInformationParameterYAML + | BitwardenCreditCardDataParameterYAML | WorkflowParameterYAML | ContextParameterYAML | OutputParameterYAML diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py index 918566ae..a7cd5c9d 100644 --- a/skyvern/forge/sdk/workflow/service.py +++ b/skyvern/forge/sdk/workflow/service.py @@ -534,6 +534,28 @@ class WorkflowService: description=description, ) + async def create_bitwarden_credit_card_data_parameter( + self, + workflow_id: str, + bitwarden_client_id_aws_secret_key: str, + bitwarden_client_secret_aws_secret_key: str, + bitwarden_master_password_aws_secret_key: str, + bitwarden_collection_id: str, + bitwarden_item_id: str, + key: str, + description: str | None = None, + ) -> Parameter: + return await app.DATABASE.create_bitwarden_credit_card_data_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, + bitwarden_collection_id=bitwarden_collection_id, + bitwarden_item_id=bitwarden_item_id, + key=key, + description=description, + ) + async def create_output_parameter( self, workflow_id: str, key: str, description: str | None = None ) -> OutputParameter: @@ -824,10 +846,11 @@ class WorkflowService: async def create_workflow_from_request( self, - organization_id: str, + organization: Organization, request: WorkflowCreateYAMLRequest, workflow_permanent_id: str | None = None, ) -> Workflow: + organization_id = organization.organization_id LOG.info( "Creating workflow from request", organization_id=organization_id, @@ -938,6 +961,21 @@ class WorkflowService: key=parameter.key, description=parameter.description, ) + elif parameter.parameter_type == ParameterType.BITWARDEN_CREDIT_CARD_DATA: + if not organization.bw_organization_id and not organization.bw_collection_ids: + raise InvalidWorkflowDefinition( + message="To use credit card data parameters, please contact us at support@skyvern.com" + ) + parameters[parameter.key] = await self.create_bitwarden_credit_card_data_parameter( + workflow_id=workflow.workflow_id, + bitwarden_client_id_aws_secret_key=parameter.bitwarden_client_id_aws_secret_key, + bitwarden_client_secret_aws_secret_key=parameter.bitwarden_client_secret_aws_secret_key, + bitwarden_master_password_aws_secret_key=parameter.bitwarden_master_password_aws_secret_key, + bitwarden_collection_id=parameter.bitwarden_collection_id, + bitwarden_item_id=parameter.bitwarden_item_id, + key=parameter.key, + description=parameter.description, + ) elif parameter.parameter_type == ParameterType.WORKFLOW: parameters[parameter.key] = await self.create_workflow_parameter( workflow_id=workflow.workflow_id,