Credit Card Parameter (#903)
This commit is contained in:
@@ -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 ###
|
||||||
@@ -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."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)}"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user