Credit Card Parameter (#903)

This commit is contained in:
Kerem Yilmaz
2024-10-03 16:18:21 -07:00
committed by GitHub
parent 83d0879034
commit b0aa181c68
11 changed files with 412 additions and 12 deletions

View File

@@ -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 ###

View File

@@ -281,9 +281,9 @@ class BitwardenSyncError(BitwardenBaseError):
class BitwardenAccessDeniedError(BitwardenBaseError): class BitwardenAccessDeniedError(BitwardenBaseError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
"Current organization does not have access to the specified 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, \ "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." "Skyvern team needs to let your Skyvern account access the Bitwarden collection."
) )

View File

@@ -15,6 +15,7 @@ from skyvern.forge.sdk.db.exceptions import NotFoundError
from skyvern.forge.sdk.db.models import ( from skyvern.forge.sdk.db.models import (
ArtifactModel, ArtifactModel,
AWSSecretParameterModel, AWSSecretParameterModel,
BitwardenCreditCardDataParameterModel,
BitwardenLoginCredentialParameterModel, BitwardenLoginCredentialParameterModel,
BitwardenSensitiveInformationParameterModel, BitwardenSensitiveInformationParameterModel,
OrganizationAuthTokenModel, 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.schemas.totp_codes import TOTPCode
from skyvern.forge.sdk.workflow.models.parameter import ( from skyvern.forge.sdk.workflow.models.parameter import (
AWSSecretParameter, AWSSecretParameter,
BitwardenCreditCardDataParameter,
BitwardenLoginCredentialParameter, BitwardenLoginCredentialParameter,
BitwardenSensitiveInformationParameter, BitwardenSensitiveInformationParameter,
OutputParameter, OutputParameter,
@@ -1218,6 +1220,33 @@ class AgentDB:
await session.refresh(bitwarden_sensitive_information_parameter) await session.refresh(bitwarden_sensitive_information_parameter)
return convert_to_bitwarden_sensitive_information_parameter(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( async def create_output_parameter(
self, self,
workflow_id: str, workflow_id: str,

View File

@@ -41,6 +41,7 @@ AWS_SECRET_PARAMETER_PREFIX = "asp"
OUTPUT_PARAMETER_PREFIX = "op" OUTPUT_PARAMETER_PREFIX = "op"
BITWARDEN_LOGIN_CREDENTIAL_PARAMETER_PREFIX = "blc" BITWARDEN_LOGIN_CREDENTIAL_PARAMETER_PREFIX = "blc"
BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX = "bsi" BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX = "bsi"
BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX = "bccd"
TASK_GENERATION_PREFIX = "tg" 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}" 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: def generate_organization_auth_token_id() -> str:
int_id = generate_id() int_id = generate_id()
return f"{ORGANIZATION_AUTH_TOKEN_PREFIX}_{int_id}" return f"{ORGANIZATION_AUTH_TOKEN_PREFIX}_{int_id}"

View File

@@ -21,6 +21,7 @@ from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.forge.sdk.db.id import ( from skyvern.forge.sdk.db.id import (
generate_artifact_id, generate_artifact_id,
generate_aws_secret_parameter_id, generate_aws_secret_parameter_id,
generate_bitwarden_credit_card_data_parameter_id,
generate_bitwarden_login_credential_parameter_id, generate_bitwarden_login_credential_parameter_id,
generate_bitwarden_sensitive_information_parameter_id, generate_bitwarden_sensitive_information_parameter_id,
generate_org_id, generate_org_id,
@@ -333,6 +334,28 @@ class BitwardenSensitiveInformationParameterModel(Base):
deleted_at = Column(DateTime, nullable=True) 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): class WorkflowRunParameterModel(Base):
__tablename__ = "workflow_run_parameters" __tablename__ = "workflow_run_parameters"

View File

@@ -668,7 +668,7 @@ async def create_workflow(
try: try:
workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml)
return await app.WORKFLOW_SERVICE.create_workflow_from_request( 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: except Exception as e:
LOG.error("Failed to create workflow", exc_info=True) LOG.error("Failed to create workflow", exc_info=True)
@@ -712,7 +712,7 @@ async def update_workflow(
try: try:
workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml)
return await app.WORKFLOW_SERVICE.create_workflow_from_request( return await app.WORKFLOW_SERVICE.create_workflow_from_request(
organization_id=current_org.organization_id, organization=current_org,
request=workflow_create_request, request=workflow_create_request,
workflow_permanent_id=workflow_permanent_id, workflow_permanent_id=workflow_permanent_id,
) )

View File

@@ -39,11 +39,19 @@ 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"
USERNAME = "BW_USERNAME" USERNAME = "BW_USERNAME"
PASSWORD = "BW_PASSWORD" PASSWORD = "BW_PASSWORD"
TOTP = "BW_TOTP" 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: class BitwardenService:
@staticmethod @staticmethod
@@ -94,6 +102,8 @@ class BitwardenService:
""" """
Get the secret value from the Bitwarden CLI. 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: try:
async with asyncio.timeout(timeout): async with asyncio.timeout(timeout):
return await BitwardenService._get_secret_value_from_url( return await BitwardenService._get_secret_value_from_url(
@@ -105,6 +115,8 @@ class BitwardenService:
url=url, url=url,
collection_id=collection_id, collection_id=collection_id,
) )
except BitwardenAccessDeniedError as e:
raise e
except Exception as e: except Exception as e:
if remaining_retries <= 0: if remaining_retries <= 0:
raise BitwardenListItemsError( raise BitwardenListItemsError(
@@ -140,8 +152,6 @@ class BitwardenService:
""" """
Get the secret value from the Bitwarden CLI. 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: try:
BitwardenService.login(client_id, client_secret) BitwardenService.login(client_id, client_secret)
BitwardenService.sync() BitwardenService.sync()
@@ -255,6 +265,8 @@ class BitwardenService:
""" """
Get the secret value from the Bitwarden CLI. 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: try:
async with asyncio.timeout(timeout): async with asyncio.timeout(timeout):
return await BitwardenService._get_sensitive_information_from_identity( return await BitwardenService._get_sensitive_information_from_identity(
@@ -267,6 +279,8 @@ class BitwardenService:
identity_key=identity_key, identity_key=identity_key,
identity_fields=identity_fields, identity_fields=identity_fields,
) )
except BitwardenAccessDeniedError as e:
raise e
except Exception as e: except Exception as e:
if remaining_retries <= 0: if remaining_retries <= 0:
raise BitwardenListItemsError( raise BitwardenListItemsError(
@@ -304,8 +318,6 @@ class BitwardenService:
""" """
Get the sensitive information from the Bitwarden CLI. 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: try:
BitwardenService.login(client_id, client_secret) BitwardenService.login(client_id, client_secret)
BitwardenService.sync() BitwardenService.sync()
@@ -438,3 +450,123 @@ class BitwardenService:
logout_result = BitwardenService.run_command(logout_command) logout_result = BitwardenService.run_command(logout_command)
if logout_result.stderr and "You are not logged in." not in logout_result.stderr: if logout_result.stderr and "You are not logged in." not in logout_result.stderr:
raise BitwardenLogoutError(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)}"],
)

View File

@@ -203,7 +203,6 @@ class WorkflowRunContext:
except BitwardenBaseError as e: except BitwardenBaseError as e:
LOG.error(f"Failed to get secret from Bitwarden. Error: {e}") LOG.error(f"Failed to get secret from Bitwarden. Error: {e}")
raise e raise e
# TODO (kerem): Implement this
elif parameter.parameter_type == ParameterType.BITWARDEN_SENSITIVE_INFORMATION: elif parameter.parameter_type == ParameterType.BITWARDEN_SENSITIVE_INFORMATION:
try: try:
# Get the Bitwarden login credentials from AWS secrets # Get the Bitwarden login credentials from AWS secrets
@@ -256,6 +255,73 @@ class WorkflowRunContext:
except BitwardenBaseError as e: except BitwardenBaseError as e:
LOG.error(f"Failed to get sensitive information from Bitwarden. Error: {e}") LOG.error(f"Failed to get sensitive information from Bitwarden. Error: {e}")
raise 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): elif isinstance(parameter, ContextParameter):
if isinstance(parameter.source, WorkflowParameter): if isinstance(parameter.source, WorkflowParameter):
# TODO (kerem): set this while initializing the context manager # TODO (kerem): set this while initializing the context manager

View File

@@ -4,7 +4,7 @@ from datetime import datetime
from enum import StrEnum from enum import StrEnum
from typing import Annotated, Literal, Union from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, ConfigDict, Field
class ParameterType(StrEnum): class ParameterType(StrEnum):
@@ -13,6 +13,7 @@ class ParameterType(StrEnum):
AWS_SECRET = "aws_secret" AWS_SECRET = "aws_secret"
BITWARDEN_LOGIN_CREDENTIAL = "bitwarden_login_credential" BITWARDEN_LOGIN_CREDENTIAL = "bitwarden_login_credential"
BITWARDEN_SENSITIVE_INFORMATION = "bitwarden_sensitive_information" BITWARDEN_SENSITIVE_INFORMATION = "bitwarden_sensitive_information"
BITWARDEN_CREDIT_CARD_DATA = "bitwarden_credit_card_data"
OUTPUT = "output" OUTPUT = "output"
@@ -86,6 +87,25 @@ class BitwardenSensitiveInformationParameter(Parameter):
deleted_at: datetime | None = None 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): class WorkflowParameterType(StrEnum):
STRING = "string" STRING = "string"
INTEGER = "integer" INTEGER = "integer"
@@ -150,6 +170,7 @@ ParameterSubclasses = Union[
AWSSecretParameter, AWSSecretParameter,
BitwardenLoginCredentialParameter, BitwardenLoginCredentialParameter,
BitwardenSensitiveInformationParameter, BitwardenSensitiveInformationParameter,
BitwardenCreditCardDataParameter,
OutputParameter, OutputParameter,
] ]
PARAMETER_TYPE = Annotated[ParameterSubclasses, Field(discriminator="parameter_type")] PARAMETER_TYPE = Annotated[ParameterSubclasses, Field(discriminator="parameter_type")]

View File

@@ -61,6 +61,21 @@ class BitwardenSensitiveInformationParameterYAML(ParameterYAML):
bitwarden_identity_fields: list[str] 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): class WorkflowParameterYAML(ParameterYAML):
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error: # 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" # Parameter 1 of Literal[...] cannot be of type "Any"
@@ -196,6 +211,7 @@ PARAMETER_YAML_SUBCLASSES = (
AWSSecretParameterYAML AWSSecretParameterYAML
| BitwardenLoginCredentialParameterYAML | BitwardenLoginCredentialParameterYAML
| BitwardenSensitiveInformationParameterYAML | BitwardenSensitiveInformationParameterYAML
| BitwardenCreditCardDataParameterYAML
| WorkflowParameterYAML | WorkflowParameterYAML
| ContextParameterYAML | ContextParameterYAML
| OutputParameterYAML | OutputParameterYAML

View File

@@ -534,6 +534,28 @@ class WorkflowService:
description=description, 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( async def create_output_parameter(
self, workflow_id: str, key: str, description: str | None = None self, workflow_id: str, key: str, description: str | None = None
) -> OutputParameter: ) -> OutputParameter:
@@ -824,10 +846,11 @@ class WorkflowService:
async def create_workflow_from_request( async def create_workflow_from_request(
self, self,
organization_id: str, organization: Organization,
request: WorkflowCreateYAMLRequest, request: WorkflowCreateYAMLRequest,
workflow_permanent_id: str | None = None, workflow_permanent_id: str | None = None,
) -> Workflow: ) -> Workflow:
organization_id = organization.organization_id
LOG.info( LOG.info(
"Creating workflow from request", "Creating workflow from request",
organization_id=organization_id, organization_id=organization_id,
@@ -938,6 +961,21 @@ class WorkflowService:
key=parameter.key, key=parameter.key,
description=parameter.description, 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: elif parameter.parameter_type == ParameterType.WORKFLOW:
parameters[parameter.key] = await self.create_workflow_parameter( parameters[parameter.key] = await self.create_workflow_parameter(
workflow_id=workflow.workflow_id, workflow_id=workflow.workflow_id,