mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-10 15:35:51 +00:00
Use bitwarden server to manage credentials (#1806)
Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
parent
902c0ad4ce
commit
02a8861d4a
11 changed files with 609 additions and 35 deletions
|
@ -0,0 +1,52 @@
|
|||
"""change credentials table, add organization_collections table
|
||||
|
||||
Revision ID: b4bb0b98912a
|
||||
Revises: 26c5ed737819
|
||||
Create Date: 2025-02-20 21:46:23.732969+00:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b4bb0b98912a"
|
||||
down_revision: Union[str, None] = "26c5ed737819"
|
||||
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(
|
||||
"organization_bitwarden_collections",
|
||||
sa.Column("organization_bitwarden_collection_id", sa.String(), nullable=False),
|
||||
sa.Column("organization_id", sa.String(), nullable=False),
|
||||
sa.Column("collection_id", sa.String(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("modified_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("organization_bitwarden_collection_id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_organization_bitwarden_collections_organization_id"),
|
||||
"organization_bitwarden_collections",
|
||||
["organization_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.add_column("credentials", sa.Column("item_id", sa.String(), nullable=True))
|
||||
op.drop_column("credentials", "website_url")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("credentials", sa.Column("website_url", sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.drop_column("credentials", "item_id")
|
||||
op.drop_index(
|
||||
op.f("ix_organization_bitwarden_collections_organization_id"), table_name="organization_bitwarden_collections"
|
||||
)
|
||||
op.drop_table("organization_bitwarden_collections")
|
||||
# ### end Alembic commands ###
|
|
@ -148,6 +148,14 @@ class Settings(BaseSettings):
|
|||
BITWARDEN_CLIENT_SECRET: str | None = None
|
||||
BITWARDEN_MASTER_PASSWORD: str | None = None
|
||||
|
||||
# Skyvern Auth Bitwarden Settings
|
||||
SKYVERN_AUTH_BITWARDEN_CLIENT_ID: str | None = None
|
||||
SKYVERN_AUTH_BITWARDEN_CLIENT_SECRET: str | None = None
|
||||
SKYVERN_AUTH_BITWARDEN_MASTER_PASSWORD: str | None = None
|
||||
SKYVERN_AUTH_BITWARDEN_ORGANIZATION_ID: str | None = None
|
||||
|
||||
BITWARDEN_SERVER_PORT: int = 8002
|
||||
|
||||
SVG_MAX_LENGTH: int = 100000
|
||||
|
||||
ENABLE_LOG_ARTIFACTS: bool = False
|
||||
|
|
|
@ -266,6 +266,11 @@ class NoFileDownloadTriggered(SkyvernException):
|
|||
super().__init__(f"Clicking on element doesn't trigger the file download. element_id={element_id}")
|
||||
|
||||
|
||||
class BitwardenSecretError(SkyvernException):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Bitwarden secret error: {message}")
|
||||
|
||||
|
||||
class BitwardenBaseError(SkyvernException):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Bitwarden error: {message}")
|
||||
|
@ -281,6 +286,31 @@ class BitwardenUnlockError(BitwardenBaseError):
|
|||
super().__init__(f"Error unlocking Bitwarden: {message}")
|
||||
|
||||
|
||||
class BitwardenCreateCollectionError(BitwardenBaseError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Error creating collection in Bitwarden: {message}")
|
||||
|
||||
|
||||
class BitwardenCreateLoginItemError(BitwardenBaseError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Error creating login item in Bitwarden: {message}")
|
||||
|
||||
|
||||
class BitwardenCreateCreditCardItemError(BitwardenBaseError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Error creating credit card item in Bitwarden: {message}")
|
||||
|
||||
|
||||
class BitwardenCreateFolderError(BitwardenBaseError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Error creating folder in Bitwarden: {message}")
|
||||
|
||||
|
||||
class BitwardenGetItemError(BitwardenBaseError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Error getting item in Bitwarden: {message}")
|
||||
|
||||
|
||||
class BitwardenListItemsError(BitwardenBaseError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(f"Error listing items in Bitwarden: {message}")
|
||||
|
|
|
@ -114,3 +114,36 @@ async def aiohttp_post(
|
|||
await asyncio.sleep(retry_timeout)
|
||||
count += 1
|
||||
raise Exception(f"Failed post request url={url}")
|
||||
|
||||
|
||||
async def aiohttp_delete(
|
||||
url: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
cookies: dict[str, str] | None = None,
|
||||
retry: int = 0,
|
||||
proxy: str | None = None,
|
||||
timeout: int = DEFAULT_REQUEST_TIMEOUT,
|
||||
raise_exception: bool = True,
|
||||
retry_timeout: float = 0,
|
||||
) -> dict[str, Any]:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session:
|
||||
count = 0
|
||||
while count <= retry:
|
||||
try:
|
||||
async with session.delete(
|
||||
url,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
proxy=proxy,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
if raise_exception:
|
||||
raise HttpException(response.status, url)
|
||||
LOG.error(f"Failed to delete data from {url}", status_code=response.status)
|
||||
return {}
|
||||
except Exception:
|
||||
if retry_timeout > 0:
|
||||
await asyncio.sleep(retry_timeout)
|
||||
count += 1
|
||||
raise Exception(f"Failed to delete data from {url}")
|
||||
|
|
|
@ -25,6 +25,7 @@ from skyvern.forge.sdk.db.models import (
|
|||
ObserverCruiseModel,
|
||||
ObserverThoughtModel,
|
||||
OrganizationAuthTokenModel,
|
||||
OrganizationBitwardenCollectionModel,
|
||||
OrganizationModel,
|
||||
OutputParameterModel,
|
||||
PersistentBrowserSessionModel,
|
||||
|
@ -63,6 +64,7 @@ from skyvern.forge.sdk.models import Step, StepStatus
|
|||
from skyvern.forge.sdk.schemas.ai_suggestions import AISuggestion
|
||||
from skyvern.forge.sdk.schemas.credentials import Credential, CredentialType
|
||||
from skyvern.forge.sdk.schemas.observers import ObserverTask, ObserverTaskStatus, ObserverThought, ObserverThoughtType
|
||||
from skyvern.forge.sdk.schemas.organization_bitwarden_collections import OrganizationBitwardenCollection
|
||||
from skyvern.forge.sdk.schemas.organizations import Organization, OrganizationAuthToken
|
||||
from skyvern.forge.sdk.schemas.persistent_browser_sessions import PersistentBrowserSession
|
||||
from skyvern.forge.sdk.schemas.task_generations import TaskGeneration
|
||||
|
@ -2745,14 +2747,18 @@ class AgentDB:
|
|||
return TaskRun.model_validate(task_run)
|
||||
|
||||
async def create_credential(
|
||||
self, name: str, website_url: str | None, credential_type: CredentialType, organization_id: str
|
||||
self,
|
||||
name: str,
|
||||
credential_type: CredentialType,
|
||||
organization_id: str,
|
||||
item_id: str,
|
||||
) -> Credential:
|
||||
async with self.Session() as session:
|
||||
credential = CredentialModel(
|
||||
organization_id=organization_id,
|
||||
name=name,
|
||||
website_url=website_url,
|
||||
credential_type=credential_type,
|
||||
item_id=item_id,
|
||||
)
|
||||
session.add(credential)
|
||||
await session.commit()
|
||||
|
@ -2773,7 +2779,7 @@ class AgentDB:
|
|||
return Credential.model_validate(credential)
|
||||
raise NotFoundError(f"Credential {credential_id} not found")
|
||||
|
||||
async def get_credentials(self, organization_id: str) -> list[Credential]:
|
||||
async def get_credentials(self, organization_id: str, page: int = 1, page_size: int = 10) -> list[Credential]:
|
||||
async with self.Session() as session:
|
||||
credentials = (
|
||||
await session.scalars(
|
||||
|
@ -2781,6 +2787,8 @@ class AgentDB:
|
|||
.filter_by(organization_id=organization_id)
|
||||
.filter(CredentialModel.deleted_at.is_(None))
|
||||
.order_by(CredentialModel.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
).all()
|
||||
return [Credential.model_validate(credential) for credential in credentials]
|
||||
|
@ -2822,6 +2830,34 @@ class AgentDB:
|
|||
await session.refresh(credential)
|
||||
return None
|
||||
|
||||
async def create_organization_bitwarden_collection(
|
||||
self,
|
||||
organization_id: str,
|
||||
collection_id: str,
|
||||
) -> OrganizationBitwardenCollection:
|
||||
async with self.Session() as session:
|
||||
organization_bitwarden_collection = OrganizationBitwardenCollectionModel(
|
||||
organization_id=organization_id, collection_id=collection_id
|
||||
)
|
||||
session.add(organization_bitwarden_collection)
|
||||
await session.commit()
|
||||
await session.refresh(organization_bitwarden_collection)
|
||||
return OrganizationBitwardenCollection.model_validate(organization_bitwarden_collection)
|
||||
|
||||
async def get_organization_bitwarden_collection(
|
||||
self,
|
||||
organization_id: str,
|
||||
) -> OrganizationBitwardenCollection | None:
|
||||
async with self.Session() as session:
|
||||
organization_bitwarden_collection = (
|
||||
await session.scalars(
|
||||
select(OrganizationBitwardenCollectionModel).filter_by(organization_id=organization_id)
|
||||
)
|
||||
).first()
|
||||
if organization_bitwarden_collection:
|
||||
return OrganizationBitwardenCollection.model_validate(organization_bitwarden_collection)
|
||||
return None
|
||||
|
||||
async def cache_task_run(self, run_id: str, organization_id: str | None = None) -> TaskRun:
|
||||
async with self.Session() as session:
|
||||
task_run = (
|
||||
|
|
|
@ -35,6 +35,8 @@ BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX = "bccd"
|
|||
BITWARDEN_LOGIN_CREDENTIAL_PARAMETER_PREFIX = "blc"
|
||||
BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX = "bsi"
|
||||
CREDENTIAL_PARAMETER_PREFIX = "cp"
|
||||
CREDENTIAL_PREFIX = "cred"
|
||||
ORGANIZATION_BITWARDEN_COLLECTION_PREFIX = "obc"
|
||||
OBSERVER_CRUISE_ID = "oc"
|
||||
OBSERVER_THOUGHT_ID = "ot"
|
||||
ORGANIZATION_AUTH_TOKEN_PREFIX = "oat"
|
||||
|
@ -52,7 +54,6 @@ WORKFLOW_PERMANENT_ID_PREFIX = "wpid"
|
|||
WORKFLOW_PREFIX = "w"
|
||||
WORKFLOW_RUN_BLOCK_PREFIX = "wrb"
|
||||
WORKFLOW_RUN_PREFIX = "wr"
|
||||
CREDENTIAL_PREFIX = "cred"
|
||||
|
||||
|
||||
def generate_workflow_id() -> str:
|
||||
|
@ -175,14 +176,19 @@ def generate_task_run_id() -> str:
|
|||
return f"{TASK_RUN_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_credential_parameter_id() -> str:
|
||||
int_id = generate_id()
|
||||
return f"{CREDENTIAL_PARAMETER_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_credential_id() -> str:
|
||||
int_id = generate_id()
|
||||
return f"{CREDENTIAL_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_credential_parameter_id() -> str:
|
||||
def generate_organization_bitwarden_collection_id() -> str:
|
||||
int_id = generate_id()
|
||||
return f"{CREDENTIAL_PARAMETER_PREFIX}_{int_id}"
|
||||
return f"{ORGANIZATION_BITWARDEN_COLLECTION_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_id() -> int:
|
||||
|
|
|
@ -33,6 +33,7 @@ from skyvern.forge.sdk.db.id import (
|
|||
generate_observer_thought_id,
|
||||
generate_org_id,
|
||||
generate_organization_auth_token_id,
|
||||
generate_organization_bitwarden_collection_id,
|
||||
generate_output_parameter_id,
|
||||
generate_persistent_browser_session_id,
|
||||
generate_step_id,
|
||||
|
@ -648,15 +649,29 @@ class TaskRunModel(Base):
|
|||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
|
||||
|
||||
class OrganizationBitwardenCollectionModel(Base):
|
||||
__tablename__ = "organization_bitwarden_collections"
|
||||
|
||||
organization_bitwarden_collection_id = Column(
|
||||
String, primary_key=True, default=generate_organization_bitwarden_collection_id
|
||||
)
|
||||
|
||||
organization_id = Column(String, nullable=False, index=True)
|
||||
collection_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)
|
||||
|
||||
|
||||
class CredentialModel(Base):
|
||||
__tablename__ = "credentials"
|
||||
|
||||
credential_id = Column(String, primary_key=True, default=generate_credential_id)
|
||||
organization_id = Column(String, nullable=False)
|
||||
item_id = Column(String, nullable=True)
|
||||
|
||||
credential_type = Column(String, nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
website_url = Column(String, nullable=True)
|
||||
credential_type = 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)
|
||||
|
|
|
@ -9,6 +9,15 @@ class CredentialType(StrEnum):
|
|||
CREDIT_CARD = "credit_card"
|
||||
|
||||
|
||||
class PasswordCredentialResponse(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
class CreditCardCredentialResponse(BaseModel):
|
||||
last_four: str
|
||||
brand: str
|
||||
|
||||
|
||||
class PasswordCredential(BaseModel):
|
||||
password: str
|
||||
username: str
|
||||
|
@ -23,27 +32,36 @@ class CreditCardCredential(BaseModel):
|
|||
card_holder_name: str
|
||||
|
||||
|
||||
class UpdateCredentialRequest(BaseModel):
|
||||
name: str | None = None
|
||||
website_url: str | None = None
|
||||
class CredentialItem(BaseModel):
|
||||
item_id: str
|
||||
name: str
|
||||
credential_type: CredentialType
|
||||
credential: PasswordCredential | CreditCardCredential
|
||||
|
||||
|
||||
class CreateCredentialRequest(BaseModel):
|
||||
name: str
|
||||
website_url: str | None = None
|
||||
credential_type: CredentialType
|
||||
credential: PasswordCredential | CreditCardCredential
|
||||
|
||||
|
||||
class CredentialResponse(BaseModel):
|
||||
credential_id: str
|
||||
credential: PasswordCredentialResponse | CreditCardCredentialResponse
|
||||
credential_type: CredentialType
|
||||
name: str
|
||||
|
||||
|
||||
class Credential(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
credential_id: str
|
||||
organization_id: str
|
||||
name: str
|
||||
website_url: str | None = None
|
||||
credential_type: CredentialType
|
||||
|
||||
item_id: str
|
||||
|
||||
created_at: datetime
|
||||
modified_at: datetime
|
||||
deleted_at: datetime | None = None
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class OrganizationBitwardenCollection(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
organization_bitwarden_collection_id: str
|
||||
organization_id: str
|
||||
collection_id: str
|
||||
created_at: datetime
|
||||
modified_at: datetime
|
|
@ -3,7 +3,8 @@ import json
|
|||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
from enum import StrEnum
|
||||
from enum import IntEnum, StrEnum
|
||||
from typing import Tuple
|
||||
|
||||
import structlog
|
||||
import tldextract
|
||||
|
@ -12,15 +13,80 @@ from pydantic import BaseModel
|
|||
from skyvern.config import settings
|
||||
from skyvern.exceptions import (
|
||||
BitwardenAccessDeniedError,
|
||||
BitwardenCreateCollectionError,
|
||||
BitwardenCreateCreditCardItemError,
|
||||
BitwardenCreateLoginItemError,
|
||||
BitwardenGetItemError,
|
||||
BitwardenListItemsError,
|
||||
BitwardenLoginError,
|
||||
BitwardenLogoutError,
|
||||
BitwardenSecretError,
|
||||
BitwardenSyncError,
|
||||
BitwardenUnlockError,
|
||||
)
|
||||
from skyvern.forge.sdk.api.aws import aws_client
|
||||
from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_delete, aiohttp_get_json, aiohttp_post
|
||||
from skyvern.forge.sdk.schemas.credentials import (
|
||||
CredentialItem,
|
||||
CredentialType,
|
||||
CreditCardCredential,
|
||||
PasswordCredential,
|
||||
)
|
||||
|
||||
|
||||
class BitwardenItemType(IntEnum):
|
||||
LOGIN = 1
|
||||
SECURE_NOTE = 2
|
||||
CREDIT_CARD = 3
|
||||
IDENTITY = 4
|
||||
|
||||
|
||||
def get_bitwarden_item_type_code(item_type: BitwardenItemType) -> int:
|
||||
if item_type == BitwardenItemType.LOGIN:
|
||||
return 1
|
||||
elif item_type == BitwardenItemType.SECURE_NOTE:
|
||||
return 2
|
||||
elif item_type == BitwardenItemType.CREDIT_CARD:
|
||||
return 3
|
||||
elif item_type == BitwardenItemType.IDENTITY:
|
||||
return 4
|
||||
|
||||
|
||||
def get_list_response_item_from_bitwarden_item(item: dict) -> CredentialItem:
|
||||
if item["type"] == BitwardenItemType.LOGIN:
|
||||
login = item["login"]
|
||||
return CredentialItem(
|
||||
item_id=item["id"],
|
||||
credential=PasswordCredential(
|
||||
username=login["username"],
|
||||
password=login["password"],
|
||||
),
|
||||
name=item["name"],
|
||||
credential_type=CredentialType.PASSWORD,
|
||||
)
|
||||
elif item["type"] == BitwardenItemType.CREDIT_CARD:
|
||||
card = item["card"]
|
||||
return CredentialItem(
|
||||
item_id=item["id"],
|
||||
credential=CreditCardCredential(
|
||||
card_holder_name=card["cardholderName"],
|
||||
card_number=card["number"],
|
||||
card_exp_month=card["expMonth"],
|
||||
card_exp_year=card["expYear"],
|
||||
card_cvv=card["code"],
|
||||
card_brand=card["brand"],
|
||||
),
|
||||
name=item["name"],
|
||||
credential_type=CredentialType.CREDIT_CARD,
|
||||
)
|
||||
else:
|
||||
raise BitwardenGetItemError(f"Unsupported item type: {item['type']}")
|
||||
|
||||
|
||||
LOG = structlog.get_logger()
|
||||
|
||||
BITWARDEN_SERVER_BASE_URL = f"http://localhost:{settings.BITWARDEN_SERVER_PORT or 8002}"
|
||||
|
||||
|
||||
def is_valid_email(email: str | None) -> bool:
|
||||
if not email:
|
||||
|
@ -52,6 +118,11 @@ class BitwardenConstants(StrEnum):
|
|||
CREDIT_CARD_CVV = "BW_CREDIT_CARD_CVV"
|
||||
CREDIT_CARD_BRAND = "BW_CREDIT_CARD_BRAND"
|
||||
|
||||
SKYVERN_AUTH_BITWARDEN_ORGANIZATION_ID = "SKYVERN_AUTH_BITWARDEN_ORGANIZATION_ID"
|
||||
SKYVERN_AUTH_BITWARDEN_MASTER_PASSWORD = "SKYVERN_AUTH_BITWARDEN_MASTER_PASSWORD"
|
||||
SKYVERN_AUTH_BITWARDEN_CLIENT_ID = "SKYVERN_AUTH_BITWARDEN_CLIENT_ID"
|
||||
SKYVERN_AUTH_BITWARDEN_CLIENT_SECRET = "SKYVERN_AUTH_BITWARDEN_CLIENT_SECRET"
|
||||
|
||||
|
||||
class BitwardenQueryResult(BaseModel):
|
||||
credential: dict[str, str]
|
||||
|
@ -629,3 +700,307 @@ class BitwardenService:
|
|||
remaining_retries=remaining_retries,
|
||||
fail_reasons=fail_reasons + [f"{type(e).__name__}: {str(e)}"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _sync_using_server() -> None:
|
||||
await aiohttp_post(f"{BITWARDEN_SERVER_BASE_URL}/sync")
|
||||
|
||||
@staticmethod
|
||||
async def _unlock_using_server(master_password: str) -> None:
|
||||
status_response = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/status")
|
||||
status = status_response["data"]["template"]["status"]
|
||||
if status != "unlocked":
|
||||
await aiohttp_post(f"{BITWARDEN_SERVER_BASE_URL}/unlock", data={"password": master_password})
|
||||
|
||||
@staticmethod
|
||||
async def _get_login_item_by_id_using_server(item_id: str) -> PasswordCredential:
|
||||
response = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/item/{item_id}")
|
||||
if not response or response.get("success") is False:
|
||||
raise BitwardenGetItemError(f"Failed to get login item by ID: {item_id}")
|
||||
|
||||
login = response["data"]["login"]
|
||||
|
||||
if not login:
|
||||
raise BitwardenGetItemError(f"Item with ID: {item_id} is not a login item")
|
||||
|
||||
return PasswordCredential(
|
||||
username=login["username"],
|
||||
password=login["password"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _create_login_item_using_server(
|
||||
bw_organization_id: str,
|
||||
collection_id: str,
|
||||
name: str,
|
||||
credential: PasswordCredential,
|
||||
) -> str:
|
||||
item_template = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/template/item")
|
||||
login_template = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/template/item.login")
|
||||
|
||||
item_template = item_template["data"]["template"]
|
||||
login_template = login_template["data"]["template"]
|
||||
|
||||
login_template["username"] = credential.username
|
||||
login_template["password"] = credential.password
|
||||
|
||||
item_template["type"] = get_bitwarden_item_type_code(BitwardenItemType.LOGIN)
|
||||
item_template["name"] = name
|
||||
item_template["login"] = login_template
|
||||
item_template["collectionIds"] = [collection_id]
|
||||
item_template["organizationId"] = bw_organization_id
|
||||
|
||||
response = await aiohttp_post(f"{BITWARDEN_SERVER_BASE_URL}/object/item", data=item_template)
|
||||
if not response or response.get("success") is False:
|
||||
raise BitwardenCreateLoginItemError("Failed to create login item")
|
||||
|
||||
return response["data"]["id"]
|
||||
|
||||
@staticmethod
|
||||
async def _create_credit_card_item_using_server(
|
||||
bw_organization_id: str,
|
||||
collection_id: str,
|
||||
name: str,
|
||||
credential: CreditCardCredential,
|
||||
) -> str:
|
||||
item_template = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/template/item")
|
||||
credit_card_template = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/template/item.card")
|
||||
|
||||
item_template = item_template["data"]["template"]
|
||||
credit_card_template = credit_card_template["data"]["template"]
|
||||
|
||||
credit_card_template["cardholderName"] = credential.card_holder_name
|
||||
credit_card_template["number"] = credential.card_number
|
||||
credit_card_template["expMonth"] = credential.card_exp_month
|
||||
credit_card_template["expYear"] = credential.card_exp_year
|
||||
credit_card_template["code"] = credential.card_cvv
|
||||
credit_card_template["brand"] = credential.card_brand
|
||||
|
||||
item_template["type"] = get_bitwarden_item_type_code(BitwardenItemType.CREDIT_CARD)
|
||||
item_template["name"] = name
|
||||
item_template["card"] = credit_card_template
|
||||
item_template["collectionIds"] = [collection_id]
|
||||
item_template["organizationId"] = bw_organization_id
|
||||
|
||||
response = await aiohttp_post(f"{BITWARDEN_SERVER_BASE_URL}/object/item", data=item_template)
|
||||
if not response or response.get("success") is False:
|
||||
raise BitwardenCreateCreditCardItemError("Failed to create credit card item")
|
||||
|
||||
return response["data"]["id"]
|
||||
|
||||
@staticmethod
|
||||
async def create_credential_item(
|
||||
collection_id: str,
|
||||
name: str,
|
||||
credential: PasswordCredential | CreditCardCredential,
|
||||
) -> str:
|
||||
try:
|
||||
master_password, bw_organization_id, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
||||
|
||||
await BitwardenService._sync_using_server()
|
||||
await BitwardenService._unlock_using_server(master_password)
|
||||
if isinstance(credential, PasswordCredential):
|
||||
return await BitwardenService._create_login_item_using_server(
|
||||
bw_organization_id=bw_organization_id,
|
||||
collection_id=collection_id,
|
||||
name=name,
|
||||
credential=credential,
|
||||
)
|
||||
else:
|
||||
return await BitwardenService._create_credit_card_item_using_server(
|
||||
bw_organization_id=bw_organization_id,
|
||||
collection_id=collection_id,
|
||||
name=name,
|
||||
credential=credential,
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def _get_skyvern_auth_master_password() -> str:
|
||||
master_password = settings.SKYVERN_AUTH_BITWARDEN_MASTER_PASSWORD
|
||||
if not master_password:
|
||||
master_password = await aws_client.get_secret(BitwardenConstants.SKYVERN_AUTH_BITWARDEN_MASTER_PASSWORD)
|
||||
if not master_password:
|
||||
raise BitwardenSecretError("Skyvern auth master password is not set")
|
||||
return master_password
|
||||
|
||||
@staticmethod
|
||||
async def _get_skyvern_auth_organization_id() -> str:
|
||||
bw_organization_id = settings.SKYVERN_AUTH_BITWARDEN_ORGANIZATION_ID
|
||||
if not bw_organization_id:
|
||||
bw_organization_id = await aws_client.get_secret(BitwardenConstants.SKYVERN_AUTH_BITWARDEN_ORGANIZATION_ID)
|
||||
if not bw_organization_id:
|
||||
raise BitwardenSecretError("Skyvern auth organization ID is not set")
|
||||
return bw_organization_id
|
||||
|
||||
@staticmethod
|
||||
async def _get_skyvern_auth_client_id() -> str:
|
||||
client_id = settings.SKYVERN_AUTH_BITWARDEN_CLIENT_ID
|
||||
if not client_id:
|
||||
client_id = await aws_client.get_secret(BitwardenConstants.SKYVERN_AUTH_BITWARDEN_CLIENT_ID)
|
||||
if not client_id:
|
||||
raise BitwardenSecretError("Skyvern auth client ID is not set")
|
||||
return client_id
|
||||
|
||||
@staticmethod
|
||||
async def _get_skyvern_auth_client_secret() -> str:
|
||||
client_secret = settings.SKYVERN_AUTH_BITWARDEN_CLIENT_SECRET
|
||||
if not client_secret:
|
||||
client_secret = await aws_client.get_secret(BitwardenConstants.SKYVERN_AUTH_BITWARDEN_CLIENT_SECRET)
|
||||
if not client_secret:
|
||||
raise BitwardenSecretError("Skyvern auth client secret is not set")
|
||||
return client_secret
|
||||
|
||||
@staticmethod
|
||||
async def create_collection(
|
||||
name: str,
|
||||
) -> str:
|
||||
"""
|
||||
Create a collection in Bitwarden and return the collection ID.
|
||||
"""
|
||||
try:
|
||||
master_password, bw_organization_id, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
||||
|
||||
await BitwardenService._sync_using_server()
|
||||
await BitwardenService._unlock_using_server(master_password)
|
||||
return await BitwardenService._create_collection_using_server(bw_organization_id, name)
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def _create_collection_using_server(bw_organization_id: str, name: str) -> str:
|
||||
collection_template_response = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/template/collection")
|
||||
collection_template = collection_template_response["data"]["template"]
|
||||
|
||||
collection_template["name"] = name
|
||||
collection_template["organizationId"] = bw_organization_id
|
||||
|
||||
response = await aiohttp_post(
|
||||
f"{BITWARDEN_SERVER_BASE_URL}/object/org-collection?organizationId={bw_organization_id}",
|
||||
data=collection_template,
|
||||
)
|
||||
if not response or response.get("success") is False:
|
||||
raise BitwardenCreateCollectionError("Failed to create collection")
|
||||
|
||||
return response["data"]["id"]
|
||||
|
||||
@staticmethod
|
||||
async def _get_skyvern_auth_secrets() -> Tuple[str, str, str, str]:
|
||||
master_password, bw_organization_id, client_id, client_secret = await asyncio.gather(
|
||||
BitwardenService._get_skyvern_auth_master_password(),
|
||||
BitwardenService._get_skyvern_auth_organization_id(),
|
||||
BitwardenService._get_skyvern_auth_client_id(),
|
||||
BitwardenService._get_skyvern_auth_client_secret(),
|
||||
)
|
||||
return master_password, bw_organization_id, client_id, client_secret
|
||||
|
||||
@staticmethod
|
||||
async def get_items_by_item_ids(
|
||||
item_ids: list[str],
|
||||
) -> list[CredentialItem]:
|
||||
try:
|
||||
master_password, _, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
||||
await BitwardenService._sync_using_server()
|
||||
await BitwardenService._unlock_using_server(master_password)
|
||||
return await BitwardenService._get_items_by_item_ids_using_server(item_ids)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def _get_items_by_item_ids_using_server(item_ids: list[str]) -> list[CredentialItem]:
|
||||
responses = await asyncio.gather(
|
||||
*[aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/item/{item_id}") for item_id in item_ids]
|
||||
)
|
||||
if not responses or any(response.get("success") is False for response in responses):
|
||||
raise BitwardenGetItemError("Failed to get collection items")
|
||||
|
||||
return [get_list_response_item_from_bitwarden_item(response["data"]) for response in responses]
|
||||
|
||||
@staticmethod
|
||||
async def get_collection_items(
|
||||
collection_id: str,
|
||||
) -> list[CredentialItem]:
|
||||
try:
|
||||
master_password, _, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
||||
await BitwardenService._sync_using_server()
|
||||
await BitwardenService._unlock_using_server(master_password)
|
||||
return await BitwardenService._get_collection_items_using_server(collection_id)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def _get_collection_items_using_server(collection_id: str) -> list[CredentialItem]:
|
||||
response = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/list/object/items?collectionId={collection_id}")
|
||||
if not response or response.get("success") is False:
|
||||
raise BitwardenGetItemError("Failed to get collection items")
|
||||
|
||||
items = response["data"]["data"]
|
||||
items = map(lambda item: get_list_response_item_from_bitwarden_item(item), items)
|
||||
return list(items)
|
||||
|
||||
@staticmethod
|
||||
async def get_credential_item(
|
||||
item_id: str,
|
||||
) -> CredentialItem:
|
||||
try:
|
||||
master_password, _, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
||||
await BitwardenService._sync_using_server()
|
||||
await BitwardenService._unlock_using_server(master_password)
|
||||
return await BitwardenService._get_credential_item_by_id_using_server(item_id)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def _get_credential_item_by_id_using_server(item_id: str) -> CredentialItem:
|
||||
response = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/item/{item_id}")
|
||||
if not response or response.get("success") is False:
|
||||
raise BitwardenGetItemError(f"Failed to get credential item by ID: {item_id}")
|
||||
|
||||
if response["data"]["type"] == BitwardenItemType.LOGIN:
|
||||
login_item = response["data"]["login"]
|
||||
name = response["data"]["name"]
|
||||
return CredentialItem(
|
||||
item_id=item_id,
|
||||
credential_type=CredentialType.PASSWORD,
|
||||
name=name,
|
||||
credential=PasswordCredential(
|
||||
username=login_item["username"],
|
||||
password=login_item["password"],
|
||||
),
|
||||
)
|
||||
elif response["data"]["type"] == BitwardenItemType.CREDIT_CARD:
|
||||
credit_card_item = response["data"]["card"]
|
||||
name = response["data"]["name"]
|
||||
return CredentialItem(
|
||||
item_id=item_id,
|
||||
credential_type=CredentialType.CREDIT_CARD,
|
||||
name=name,
|
||||
credential=CreditCardCredential(
|
||||
card_holder_name=credit_card_item["cardholderName"],
|
||||
card_number=credit_card_item["number"],
|
||||
card_exp_month=credit_card_item["expMonth"],
|
||||
card_exp_year=credit_card_item["expYear"],
|
||||
card_cvv=credit_card_item["code"],
|
||||
card_brand=credit_card_item["brand"],
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise BitwardenGetItemError(f"Unsupported item type: {response['data']['type']}")
|
||||
|
||||
@staticmethod
|
||||
async def delete_credential_item(
|
||||
item_id: str,
|
||||
) -> None:
|
||||
try:
|
||||
master_password, _, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
||||
await BitwardenService._sync_using_server()
|
||||
await BitwardenService._unlock_using_server(master_password)
|
||||
await BitwardenService._delete_credential_item_using_server(item_id)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def _delete_credential_item_using_server(item_id: str) -> None:
|
||||
await aiohttp_delete(f"{BITWARDEN_SERVER_BASE_URL}/object/item/{item_id}")
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
|
@ -8,10 +7,10 @@ from skyvern.config import settings
|
|||
from skyvern.exceptions import (
|
||||
BitwardenBaseError,
|
||||
CredentialParameterNotFoundError,
|
||||
CredentialParameterParsingError,
|
||||
SkyvernException,
|
||||
WorkflowRunContextNotInitialized,
|
||||
)
|
||||
from skyvern.forge import app
|
||||
from skyvern.forge.sdk.api.aws import AsyncAWSClient
|
||||
from skyvern.forge.sdk.schemas.organizations import Organization
|
||||
from skyvern.forge.sdk.schemas.tasks import TaskStatus
|
||||
|
@ -78,9 +77,7 @@ class WorkflowRunContext:
|
|||
if isinstance(secrete_parameter, AWSSecretParameter):
|
||||
await workflow_run_context.register_aws_secret_parameter_value(aws_client, secrete_parameter)
|
||||
elif isinstance(secrete_parameter, CredentialParameter):
|
||||
await workflow_run_context.register_credential_parameter_value(
|
||||
aws_client, secrete_parameter, organization
|
||||
)
|
||||
await workflow_run_context.register_credential_parameter_value(secrete_parameter, organization)
|
||||
elif isinstance(secrete_parameter, BitwardenLoginCredentialParameter):
|
||||
await workflow_run_context.register_bitwarden_login_credential_parameter_value(
|
||||
aws_client, secrete_parameter, organization
|
||||
|
@ -176,30 +173,21 @@ class WorkflowRunContext:
|
|||
|
||||
async def register_credential_parameter_value(
|
||||
self,
|
||||
aws_client: AsyncAWSClient,
|
||||
parameter: CredentialParameter,
|
||||
organization: Organization,
|
||||
) -> None:
|
||||
LOG.info(f"Fetching credential parameter value for credential: {parameter.credential_id}")
|
||||
org_secret_values = await aws_client.get_secret(organization.organization_id)
|
||||
if org_secret_values is None:
|
||||
raise CredentialParameterNotFoundError(parameter.credential_id)
|
||||
# Parse the items and extract credentials
|
||||
try:
|
||||
org_secret_values_json = json.loads(org_secret_values)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
raise CredentialParameterParsingError(
|
||||
f"Failed to parse credential JSON. Credential ID: {parameter.credential_id}"
|
||||
credential_item = await app.DATABASE.get_credential(
|
||||
parameter.credential_id, organization_id=organization.organization_id
|
||||
)
|
||||
|
||||
credentials = org_secret_values_json.get(parameter.credential_id)
|
||||
if credentials is None:
|
||||
if credential_item is None:
|
||||
raise CredentialParameterNotFoundError(parameter.credential_id)
|
||||
credential = await BitwardenService.get_credential_item(credential_item.item_id)
|
||||
credential_dict = credential.credential.model_dump()
|
||||
|
||||
self.parameters[parameter.key] = parameter
|
||||
self.values[parameter.key] = {}
|
||||
for key, value in credentials.items():
|
||||
for key, value in credential_dict.items():
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
secret_id = f"{random_secret_id}_{key}"
|
||||
self.secrets[secret_id] = value
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue