mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-02 18:50:24 +00:00
Implement BitwardenLoginCredentialParameter (#151)
This commit is contained in:
parent
999eda9b5d
commit
1d1e29b813
12 changed files with 392 additions and 8 deletions
|
@ -0,0 +1,67 @@
|
||||||
|
"""Create bitwarden credential parameter table
|
||||||
|
|
||||||
|
Revision ID: 4630ab8c198e
|
||||||
|
Revises: ffe2f57bd288
|
||||||
|
Create Date: 2024-04-03 22:57:03.231654+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "4630ab8c198e"
|
||||||
|
down_revision: Union[str, None] = "ffe2f57bd288"
|
||||||
|
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_login_credential_parameters",
|
||||||
|
sa.Column("bitwarden_login_credential_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("url_parameter_key", 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_login_credential_parameter_id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_bitwarden_login_credential_parameters_bitwarden_login_credential_parameter_id"),
|
||||||
|
"bitwarden_login_credential_parameters",
|
||||||
|
["bitwarden_login_credential_parameter_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_bitwarden_login_credential_parameters_workflow_id"),
|
||||||
|
"bitwarden_login_credential_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_login_credential_parameters_workflow_id"), table_name="bitwarden_login_credential_parameters"
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_bitwarden_login_credential_parameters_bitwarden_login_credential_parameter_id"),
|
||||||
|
table_name="bitwarden_login_credential_parameters",
|
||||||
|
)
|
||||||
|
op.drop_table("bitwarden_login_credential_parameters")
|
||||||
|
# ### end Alembic commands ###
|
|
@ -185,3 +185,28 @@ class DownloadFileMaxSizeExceeded(SkyvernException):
|
||||||
def __init__(self, max_size: int) -> None:
|
def __init__(self, max_size: int) -> None:
|
||||||
self.max_size = max_size
|
self.max_size = max_size
|
||||||
super().__init__(f"Download file size exceeded the maximum allowed size of {max_size} MB.")
|
super().__init__(f"Download file size exceeded the maximum allowed size of {max_size} MB.")
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenBaseError(SkyvernException):
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(f"Bitwarden error: {message}")
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenLoginError(BitwardenBaseError):
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(f"Error logging in to Bitwarden: {message}")
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenUnlockError(BitwardenBaseError):
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(f"Error unlocking Bitwarden: {message}")
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenListItemsError(BitwardenBaseError):
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(f"Error listing items in Bitwarden: {message}")
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenLogoutError(BitwardenBaseError):
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(f"Error logging out of Bitwarden: {message}")
|
||||||
|
|
|
@ -13,6 +13,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,
|
||||||
|
BitwardenLoginCredentialParameterModel,
|
||||||
OrganizationAuthTokenModel,
|
OrganizationAuthTokenModel,
|
||||||
OrganizationModel,
|
OrganizationModel,
|
||||||
OutputParameterModel,
|
OutputParameterModel,
|
||||||
|
@ -28,6 +29,7 @@ from skyvern.forge.sdk.db.utils import (
|
||||||
_custom_json_serializer,
|
_custom_json_serializer,
|
||||||
convert_to_artifact,
|
convert_to_artifact,
|
||||||
convert_to_aws_secret_parameter,
|
convert_to_aws_secret_parameter,
|
||||||
|
convert_to_bitwarden_login_credential_parameter,
|
||||||
convert_to_organization,
|
convert_to_organization,
|
||||||
convert_to_organization_auth_token,
|
convert_to_organization_auth_token,
|
||||||
convert_to_output_parameter,
|
convert_to_output_parameter,
|
||||||
|
@ -43,6 +45,7 @@ from skyvern.forge.sdk.models import Organization, OrganizationAuthToken, Step,
|
||||||
from skyvern.forge.sdk.schemas.tasks import ProxyLocation, Task, TaskStatus
|
from skyvern.forge.sdk.schemas.tasks import ProxyLocation, Task, TaskStatus
|
||||||
from skyvern.forge.sdk.workflow.models.parameter import (
|
from skyvern.forge.sdk.workflow.models.parameter import (
|
||||||
AWSSecretParameter,
|
AWSSecretParameter,
|
||||||
|
BitwardenLoginCredentialParameter,
|
||||||
OutputParameter,
|
OutputParameter,
|
||||||
WorkflowParameter,
|
WorkflowParameter,
|
||||||
WorkflowParameterType,
|
WorkflowParameterType,
|
||||||
|
@ -844,6 +847,31 @@ class AgentDB:
|
||||||
await session.refresh(aws_secret_parameter)
|
await session.refresh(aws_secret_parameter)
|
||||||
return convert_to_aws_secret_parameter(aws_secret_parameter)
|
return convert_to_aws_secret_parameter(aws_secret_parameter)
|
||||||
|
|
||||||
|
async def create_bitwarden_login_credential_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,
|
||||||
|
url_parameter_key: str,
|
||||||
|
key: str,
|
||||||
|
description: str | None = None,
|
||||||
|
) -> BitwardenLoginCredentialParameter:
|
||||||
|
async with self.Session() as session:
|
||||||
|
bitwarden_login_credential_parameter = BitwardenLoginCredentialParameterModel(
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
bitwarden_client_id_aws_secret_key=bitwarden_client_id_aws_secret_key,
|
||||||
|
bitwarden_client_secret_aws_secret_key=bitwarden_client_secret_aws_secret_key,
|
||||||
|
bitwarden_master_password_aws_secret_key=bitwarden_master_password_aws_secret_key,
|
||||||
|
url_parameter_key=url_parameter_key,
|
||||||
|
key=key,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
session.add(bitwarden_login_credential_parameter)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(bitwarden_login_credential_parameter)
|
||||||
|
return convert_to_bitwarden_login_credential_parameter(bitwarden_login_credential_parameter)
|
||||||
|
|
||||||
async def create_output_parameter(
|
async def create_output_parameter(
|
||||||
self,
|
self,
|
||||||
workflow_id: str,
|
workflow_id: str,
|
||||||
|
|
|
@ -38,6 +38,7 @@ WORKFLOW_RUN_PREFIX = "wr"
|
||||||
WORKFLOW_PARAMETER_PREFIX = "wp"
|
WORKFLOW_PARAMETER_PREFIX = "wp"
|
||||||
AWS_SECRET_PARAMETER_PREFIX = "asp"
|
AWS_SECRET_PARAMETER_PREFIX = "asp"
|
||||||
OUTPUT_PARAMETER_PREFIX = "op"
|
OUTPUT_PARAMETER_PREFIX = "op"
|
||||||
|
BITWARDEN_LOGIN_CREDENTIAL_PARAMETER_PREFIX = "blc"
|
||||||
|
|
||||||
|
|
||||||
def generate_workflow_id() -> str:
|
def generate_workflow_id() -> str:
|
||||||
|
@ -65,6 +66,11 @@ def generate_output_parameter_id() -> str:
|
||||||
return f"{OUTPUT_PARAMETER_PREFIX}_{int_id}"
|
return f"{OUTPUT_PARAMETER_PREFIX}_{int_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_bitwarden_login_credential_parameter_id() -> str:
|
||||||
|
int_id = generate_id()
|
||||||
|
return f"{BITWARDEN_LOGIN_CREDENTIAL_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}"
|
||||||
|
|
|
@ -8,6 +8,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_login_credential_parameter_id,
|
||||||
generate_org_id,
|
generate_org_id,
|
||||||
generate_organization_auth_token_id,
|
generate_organization_auth_token_id,
|
||||||
generate_output_parameter_id,
|
generate_output_parameter_id,
|
||||||
|
@ -177,6 +178,24 @@ class AWSSecretParameterModel(Base):
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
deleted_at = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenLoginCredentialParameterModel(Base):
|
||||||
|
__tablename__ = "bitwarden_login_credential_parameters"
|
||||||
|
|
||||||
|
bitwarden_login_credential_parameter_id = Column(
|
||||||
|
String, primary_key=True, index=True, default=generate_bitwarden_login_credential_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)
|
||||||
|
url_parameter_key = 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"
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
|
||||||
from skyvern.forge.sdk.db.models import (
|
from skyvern.forge.sdk.db.models import (
|
||||||
ArtifactModel,
|
ArtifactModel,
|
||||||
AWSSecretParameterModel,
|
AWSSecretParameterModel,
|
||||||
|
BitwardenLoginCredentialParameterModel,
|
||||||
OrganizationAuthTokenModel,
|
OrganizationAuthTokenModel,
|
||||||
OrganizationModel,
|
OrganizationModel,
|
||||||
OutputParameterModel,
|
OutputParameterModel,
|
||||||
|
@ -24,6 +25,7 @@ from skyvern.forge.sdk.models import Organization, OrganizationAuthToken, Step,
|
||||||
from skyvern.forge.sdk.schemas.tasks import ProxyLocation, Task, TaskStatus
|
from skyvern.forge.sdk.schemas.tasks import ProxyLocation, Task, TaskStatus
|
||||||
from skyvern.forge.sdk.workflow.models.parameter import (
|
from skyvern.forge.sdk.workflow.models.parameter import (
|
||||||
AWSSecretParameter,
|
AWSSecretParameter,
|
||||||
|
BitwardenLoginCredentialParameter,
|
||||||
OutputParameter,
|
OutputParameter,
|
||||||
WorkflowParameter,
|
WorkflowParameter,
|
||||||
WorkflowParameterType,
|
WorkflowParameterType,
|
||||||
|
@ -211,6 +213,30 @@ def convert_to_aws_secret_parameter(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_bitwarden_login_credential_parameter(
|
||||||
|
bitwarden_login_credential_parameter_model: BitwardenLoginCredentialParameterModel, debug_enabled: bool = False
|
||||||
|
) -> BitwardenLoginCredentialParameter:
|
||||||
|
if debug_enabled:
|
||||||
|
LOG.debug(
|
||||||
|
"Converting BitwardenLoginCredentialParameterModel to BitwardenLoginCredentialParameter",
|
||||||
|
bitwarden_login_credential_parameter_id=bitwarden_login_credential_parameter_model.bitwarden_login_credential_parameter_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return BitwardenLoginCredentialParameter(
|
||||||
|
bitwarden_login_credential_parameter_id=bitwarden_login_credential_parameter_model.bitwarden_login_credential_parameter_id,
|
||||||
|
workflow_id=bitwarden_login_credential_parameter_model.workflow_id,
|
||||||
|
key=bitwarden_login_credential_parameter_model.key,
|
||||||
|
description=bitwarden_login_credential_parameter_model.description,
|
||||||
|
bitwarden_client_id_aws_secret_key=bitwarden_login_credential_parameter_model.bitwarden_client_id_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,
|
||||||
|
url_parameter_key=bitwarden_login_credential_parameter_model.url_parameter_key,
|
||||||
|
created_at=bitwarden_login_credential_parameter_model.created_at,
|
||||||
|
modified_at=bitwarden_login_credential_parameter_model.modified_at,
|
||||||
|
deleted_at=bitwarden_login_credential_parameter_model.deleted_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_to_output_parameter(
|
def convert_to_output_parameter(
|
||||||
output_parameter_model: OutputParameterModel, debug_enabled: bool = False
|
output_parameter_model: OutputParameterModel, debug_enabled: bool = False
|
||||||
) -> OutputParameter:
|
) -> OutputParameter:
|
||||||
|
|
97
skyvern/forge/sdk/services/bitwarden.py
Normal file
97
skyvern/forge/sdk/services/bitwarden.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from skyvern.exceptions import BitwardenListItemsError, BitwardenLoginError, BitwardenLogoutError, BitwardenUnlockError
|
||||||
|
|
||||||
|
LOG = structlog.get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenService:
|
||||||
|
@staticmethod
|
||||||
|
def run_command(command: list[str], additional_env: dict[str, str] | None = None) -> subprocess.CompletedProcess:
|
||||||
|
"""
|
||||||
|
Run a CLI command with the specified additional environment variables and return the result.
|
||||||
|
"""
|
||||||
|
env = os.environ.copy() # Copy the current environment
|
||||||
|
# Make sure node isn't returning warnings. Warnings are sent through stderr and we raise exceptions on stderr.
|
||||||
|
env["NODE_NO_WARNINGS"] = "1"
|
||||||
|
if additional_env:
|
||||||
|
env.update(additional_env) # Update with any additional environment variables
|
||||||
|
|
||||||
|
return subprocess.run(command, capture_output=True, text=True, env=env)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_secret_value_from_url(
|
||||||
|
client_id: str,
|
||||||
|
client_secret: str,
|
||||||
|
master_password: str,
|
||||||
|
url: str,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get the secret value from the Bitwarden CLI.
|
||||||
|
"""
|
||||||
|
# Step 1: Set up environment variables and log in
|
||||||
|
env = {"BW_CLIENTID": client_id, "BW_CLIENTSECRET": client_secret, "BW_PASSWORD": master_password}
|
||||||
|
login_command = ["bw", "login", "--apikey"]
|
||||||
|
login_result = BitwardenService.run_command(login_command, env)
|
||||||
|
|
||||||
|
# Print both stdout and stderr for debugging
|
||||||
|
if login_result.stderr:
|
||||||
|
raise BitwardenLoginError(login_result.stderr)
|
||||||
|
|
||||||
|
# Step 2: Unlock the vault
|
||||||
|
unlock_command = ["bw", "unlock", "--passwordenv", "BW_PASSWORD"]
|
||||||
|
unlock_result = BitwardenService.run_command(unlock_command, env)
|
||||||
|
|
||||||
|
if unlock_result.stderr:
|
||||||
|
raise BitwardenUnlockError(unlock_result.stderr)
|
||||||
|
|
||||||
|
# Extract session key
|
||||||
|
try:
|
||||||
|
session_key = unlock_result.stdout.split('"')[1]
|
||||||
|
except IndexError:
|
||||||
|
raise BitwardenUnlockError("Unable to extract session key.")
|
||||||
|
|
||||||
|
if not session_key:
|
||||||
|
raise BitwardenUnlockError("Session key is empty.")
|
||||||
|
|
||||||
|
# Step 3: Retrieve the items
|
||||||
|
list_command = ["bw", "list", "items", "--url", url, "--session", session_key]
|
||||||
|
items_result = BitwardenService.run_command(list_command)
|
||||||
|
|
||||||
|
if items_result.stderr:
|
||||||
|
raise BitwardenListItemsError(items_result.stderr)
|
||||||
|
|
||||||
|
# Parse the items and extract credentials
|
||||||
|
try:
|
||||||
|
items = json.loads(items_result.stdout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise BitwardenListItemsError("Failed to parse items JSON. Output: " + items_result.stdout)
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
raise BitwardenListItemsError("No items found in Bitwarden.")
|
||||||
|
|
||||||
|
credentials = [
|
||||||
|
{"username": item["login"]["username"], "password": item["login"]["password"]}
|
||||||
|
for item in items
|
||||||
|
if "login" in item
|
||||||
|
]
|
||||||
|
|
||||||
|
# Step 4: Log out
|
||||||
|
BitwardenService.logout()
|
||||||
|
|
||||||
|
# Todo: Handle multiple credentials, for now just return the last one
|
||||||
|
return credentials[-1] if credentials else {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def logout() -> None:
|
||||||
|
"""
|
||||||
|
Log out of the Bitwarden CLI.
|
||||||
|
"""
|
||||||
|
logout_command = ["bw", "logout"]
|
||||||
|
logout_result = BitwardenService.run_command(logout_command)
|
||||||
|
if logout_result.stderr:
|
||||||
|
raise BitwardenLogoutError(logout_result.stderr)
|
|
@ -3,8 +3,9 @@ from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from skyvern.exceptions import WorkflowRunContextNotInitialized
|
from skyvern.exceptions import BitwardenBaseError, WorkflowRunContextNotInitialized
|
||||||
from skyvern.forge.sdk.api.aws import AsyncAWSClient
|
from skyvern.forge.sdk.api.aws import AsyncAWSClient
|
||||||
|
from skyvern.forge.sdk.services.bitwarden import BitwardenService
|
||||||
from skyvern.forge.sdk.workflow.exceptions import OutputParameterKeyCollisionError
|
from skyvern.forge.sdk.workflow.exceptions import OutputParameterKeyCollisionError
|
||||||
from skyvern.forge.sdk.workflow.models.parameter import (
|
from skyvern.forge.sdk.workflow.models.parameter import (
|
||||||
PARAMETER_TYPE,
|
PARAMETER_TYPE,
|
||||||
|
@ -113,6 +114,46 @@ class WorkflowRunContext:
|
||||||
random_secret_id = self.generate_random_secret_id()
|
random_secret_id = self.generate_random_secret_id()
|
||||||
self.secrets[random_secret_id] = secret_value
|
self.secrets[random_secret_id] = secret_value
|
||||||
self.values[parameter.key] = random_secret_id
|
self.values[parameter.key] = random_secret_id
|
||||||
|
elif parameter.parameter_type == ParameterType.BITWARDEN_LOGIN_CREDENTIAL:
|
||||||
|
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.url_parameter_key) and self.has_value(parameter.url_parameter_key):
|
||||||
|
url = self.values[parameter.url_parameter_key]
|
||||||
|
else:
|
||||||
|
LOG.error(f"URL parameter {parameter.url_parameter_key} not found or has no value")
|
||||||
|
raise ValueError(f"URL parameter for Bitwarden login credentials not found or has no value")
|
||||||
|
|
||||||
|
try:
|
||||||
|
secret_credentials = BitwardenService.get_secret_value_from_url(
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
master_password,
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
if secret_credentials:
|
||||||
|
random_secret_id = self.generate_random_secret_id()
|
||||||
|
# username secret
|
||||||
|
username_secret_id = f"{random_secret_id}_username"
|
||||||
|
self.secrets[username_secret_id] = secret_credentials["username"]
|
||||||
|
# password secret
|
||||||
|
password_secret_id = f"{random_secret_id}_password"
|
||||||
|
self.secrets[password_secret_id] = secret_credentials["password"]
|
||||||
|
|
||||||
|
self.values[parameter.key] = {
|
||||||
|
"username": username_secret_id,
|
||||||
|
"password": password_secret_id,
|
||||||
|
}
|
||||||
|
except BitwardenBaseError as e:
|
||||||
|
BitwardenService.logout()
|
||||||
|
LOG.error(f"Failed to get secret from Bitwarden. Error: {e}")
|
||||||
|
raise e
|
||||||
elif parameter.parameter_type == ParameterType.CONTEXT:
|
elif parameter.parameter_type == ParameterType.CONTEXT:
|
||||||
# ContextParameter values will be set within the blocks
|
# ContextParameter values will be set within the blocks
|
||||||
return
|
return
|
||||||
|
@ -133,6 +174,9 @@ class WorkflowRunContext:
|
||||||
aws_client: AsyncAWSClient,
|
aws_client: AsyncAWSClient,
|
||||||
parameters: list[PARAMETER_TYPE],
|
parameters: list[PARAMETER_TYPE],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# BitwardenLoginCredentialParameter should be processed last since it requires the URL parameter to be set
|
||||||
|
parameters.sort(key=lambda x: x.parameter_type != ParameterType.BITWARDEN_LOGIN_CREDENTIAL)
|
||||||
|
|
||||||
for parameter in parameters:
|
for parameter in parameters:
|
||||||
if parameter.key in self.parameters:
|
if parameter.key in self.parameters:
|
||||||
LOG.debug(f"Parameter {parameter.key} already registered, skipping")
|
LOG.debug(f"Parameter {parameter.key} already registered, skipping")
|
||||||
|
|
|
@ -275,7 +275,7 @@ class ForLoopBlock(Block):
|
||||||
return context_parameters
|
return context_parameters
|
||||||
|
|
||||||
def get_loop_over_parameter_values(self, workflow_run_context: WorkflowRunContext) -> list[Any]:
|
def get_loop_over_parameter_values(self, workflow_run_context: WorkflowRunContext) -> list[Any]:
|
||||||
if isinstance(self.loop_over, WorkflowParameter):
|
if isinstance(self.loop_over, WorkflowParameter) or isinstance(self.loop_over, OutputParameter):
|
||||||
parameter_value = workflow_run_context.get_value(self.loop_over.key)
|
parameter_value = workflow_run_context.get_value(self.loop_over.key)
|
||||||
if isinstance(parameter_value, list):
|
if isinstance(parameter_value, list):
|
||||||
return parameter_value
|
return parameter_value
|
||||||
|
@ -284,7 +284,6 @@ class ForLoopBlock(Block):
|
||||||
return [parameter_value]
|
return [parameter_value]
|
||||||
else:
|
else:
|
||||||
# TODO (kerem): Implement this for context parameters
|
# TODO (kerem): Implement this for context parameters
|
||||||
# TODO (kerem): Implement this for output parameters
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def execute(self, workflow_run_id: str, **kwargs: dict) -> OutputParameter | None:
|
async def execute(self, workflow_run_id: str, **kwargs: dict) -> OutputParameter | None:
|
||||||
|
|
|
@ -11,6 +11,7 @@ class ParameterType(StrEnum):
|
||||||
WORKFLOW = "workflow"
|
WORKFLOW = "workflow"
|
||||||
CONTEXT = "context"
|
CONTEXT = "context"
|
||||||
AWS_SECRET = "aws_secret"
|
AWS_SECRET = "aws_secret"
|
||||||
|
BITWARDEN_LOGIN_CREDENTIAL = "bitwarden_login_credential"
|
||||||
OUTPUT = "output"
|
OUTPUT = "output"
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +38,23 @@ class AWSSecretParameter(Parameter):
|
||||||
deleted_at: datetime | None = None
|
deleted_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenLoginCredentialParameter(Parameter):
|
||||||
|
parameter_type: Literal[ParameterType.BITWARDEN_LOGIN_CREDENTIAL] = ParameterType.BITWARDEN_LOGIN_CREDENTIAL
|
||||||
|
# parameter fields
|
||||||
|
bitwarden_login_credential_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
|
||||||
|
# url to request the login credentials from bitwarden
|
||||||
|
url_parameter_key: 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"
|
||||||
|
@ -92,5 +110,7 @@ class OutputParameter(Parameter):
|
||||||
deleted_at: datetime | None = None
|
deleted_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
ParameterSubclasses = Union[WorkflowParameter, ContextParameter, AWSSecretParameter, OutputParameter]
|
ParameterSubclasses = Union[
|
||||||
|
WorkflowParameter, ContextParameter, AWSSecretParameter, BitwardenLoginCredentialParameter, OutputParameter
|
||||||
|
]
|
||||||
PARAMETER_TYPE = Annotated[ParameterSubclasses, Field(discriminator="parameter_type")]
|
PARAMETER_TYPE = Annotated[ParameterSubclasses, Field(discriminator="parameter_type")]
|
||||||
|
|
|
@ -22,6 +22,21 @@ class AWSSecretParameterYAML(ParameterYAML):
|
||||||
aws_key: str
|
aws_key: str
|
||||||
|
|
||||||
|
|
||||||
|
class BitwardenLoginCredentialParameterYAML(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_LOGIN_CREDENTIAL] = ParameterType.BITWARDEN_LOGIN_CREDENTIAL # 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
|
||||||
|
# parameter key for the url to request the login credentials from bitwarden
|
||||||
|
url_parameter_key: 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"
|
||||||
|
@ -80,7 +95,7 @@ class ForLoopBlockYAML(BlockYAML):
|
||||||
block_type: Literal[BlockType.FOR_LOOP] = BlockType.FOR_LOOP # type: ignore
|
block_type: Literal[BlockType.FOR_LOOP] = BlockType.FOR_LOOP # type: ignore
|
||||||
|
|
||||||
loop_over_parameter_key: str
|
loop_over_parameter_key: str
|
||||||
loop_block: BlockYAML
|
loop_block: "BLOCK_YAML_SUBCLASSES"
|
||||||
|
|
||||||
|
|
||||||
class CodeBlockYAML(BlockYAML):
|
class CodeBlockYAML(BlockYAML):
|
||||||
|
@ -135,7 +150,13 @@ class SendEmailBlockYAML(BlockYAML):
|
||||||
file_attachments: list[str] | None = None
|
file_attachments: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
PARAMETER_YAML_SUBCLASSES = AWSSecretParameterYAML | WorkflowParameterYAML | ContextParameterYAML | OutputParameterYAML
|
PARAMETER_YAML_SUBCLASSES = (
|
||||||
|
AWSSecretParameterYAML
|
||||||
|
| BitwardenLoginCredentialParameterYAML
|
||||||
|
| WorkflowParameterYAML
|
||||||
|
| ContextParameterYAML
|
||||||
|
| OutputParameterYAML
|
||||||
|
)
|
||||||
PARAMETER_YAML_TYPES = Annotated[PARAMETER_YAML_SUBCLASSES, Field(discriminator="parameter_type")]
|
PARAMETER_YAML_TYPES = Annotated[PARAMETER_YAML_SUBCLASSES, Field(discriminator="parameter_type")]
|
||||||
|
|
||||||
BLOCK_YAML_SUBCLASSES = (
|
BLOCK_YAML_SUBCLASSES = (
|
||||||
|
|
|
@ -351,6 +351,26 @@ class WorkflowService:
|
||||||
workflow_id=workflow_id, aws_key=aws_key, key=key, description=description
|
workflow_id=workflow_id, aws_key=aws_key, key=key, description=description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def create_bitwarden_login_credential_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,
|
||||||
|
url_parameter_key: str,
|
||||||
|
key: str,
|
||||||
|
description: str | None = None,
|
||||||
|
) -> Parameter:
|
||||||
|
return await app.DATABASE.create_bitwarden_login_credential_parameter(
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
bitwarden_client_id_aws_secret_key=bitwarden_client_id_aws_secret_key,
|
||||||
|
bitwarden_client_secret_aws_secret_key=bitwarden_client_secret_aws_secret_key,
|
||||||
|
bitwarden_master_password_aws_secret_key=bitwarden_master_password_aws_secret_key,
|
||||||
|
url_parameter_key=url_parameter_key,
|
||||||
|
key=key,
|
||||||
|
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:
|
||||||
|
@ -643,6 +663,16 @@ class WorkflowService:
|
||||||
key=parameter.key,
|
key=parameter.key,
|
||||||
description=parameter.description,
|
description=parameter.description,
|
||||||
)
|
)
|
||||||
|
elif parameter.parameter_type == ParameterType.BITWARDEN_LOGIN_CREDENTIAL:
|
||||||
|
parameters[parameter.key] = await self.create_bitwarden_login_credential_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,
|
||||||
|
url_parameter_key=parameter.url_parameter_key,
|
||||||
|
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,
|
||||||
|
@ -708,10 +738,12 @@ class WorkflowService:
|
||||||
max_retries=block_yaml.max_retries,
|
max_retries=block_yaml.max_retries,
|
||||||
)
|
)
|
||||||
elif block_yaml.block_type == BlockType.FOR_LOOP:
|
elif block_yaml.block_type == BlockType.FOR_LOOP:
|
||||||
|
loop_block = await WorkflowService.block_yaml_to_block(block_yaml.loop_block, parameters)
|
||||||
|
loop_over_parameter = parameters[block_yaml.loop_over_parameter_key]
|
||||||
return ForLoopBlock(
|
return ForLoopBlock(
|
||||||
label=block_yaml.label,
|
label=block_yaml.label,
|
||||||
loop_over_parameter_key=parameters[block_yaml.loop_over_parameter_key],
|
loop_over=loop_over_parameter,
|
||||||
loop_block=WorkflowService.block_yaml_to_block(block_yaml.loop_block, parameters),
|
loop_block=loop_block,
|
||||||
output_parameter=output_parameter,
|
output_parameter=output_parameter,
|
||||||
)
|
)
|
||||||
elif block_yaml.block_type == BlockType.CODE:
|
elif block_yaml.block_type == BlockType.CODE:
|
||||||
|
|
Loading…
Add table
Reference in a new issue