tested 1pass backend and vars (#2690)
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
committed by
GitHub
parent
b5bf9d291f
commit
9868750de3
@@ -22,6 +22,7 @@ from skyvern.forge.sdk.db.models import (
|
||||
BitwardenSensitiveInformationParameterModel,
|
||||
CredentialModel,
|
||||
CredentialParameterModel,
|
||||
OnePasswordCredentialParameterModel,
|
||||
OrganizationAuthTokenModel,
|
||||
OrganizationBitwardenCollectionModel,
|
||||
OrganizationModel,
|
||||
@@ -80,6 +81,7 @@ from skyvern.forge.sdk.workflow.models.parameter import (
|
||||
BitwardenLoginCredentialParameter,
|
||||
BitwardenSensitiveInformationParameter,
|
||||
CredentialParameter,
|
||||
OnePasswordCredentialParameter,
|
||||
OutputParameter,
|
||||
WorkflowParameter,
|
||||
WorkflowParameterType,
|
||||
@@ -1904,6 +1906,32 @@ class AgentDB:
|
||||
deleted_at=credential_parameter.deleted_at,
|
||||
)
|
||||
|
||||
async def create_onepassword_credential_parameter(
|
||||
self, workflow_id: str, key: str, vault_id: str, item_id: str, description: str | None = None
|
||||
) -> OnePasswordCredentialParameter:
|
||||
async with self.Session() as session:
|
||||
parameter = OnePasswordCredentialParameterModel(
|
||||
workflow_id=workflow_id,
|
||||
key=key,
|
||||
description=description,
|
||||
vault_id=vault_id,
|
||||
item_id=item_id,
|
||||
)
|
||||
session.add(parameter)
|
||||
await session.commit()
|
||||
await session.refresh(parameter)
|
||||
return OnePasswordCredentialParameter(
|
||||
onepassword_credential_parameter_id=parameter.onepassword_credential_parameter_id,
|
||||
workflow_id=parameter.workflow_id,
|
||||
key=parameter.key,
|
||||
description=parameter.description,
|
||||
vault_id=parameter.vault_id,
|
||||
item_id=parameter.item_id,
|
||||
created_at=parameter.created_at,
|
||||
modified_at=parameter.modified_at,
|
||||
deleted_at=parameter.deleted_at,
|
||||
)
|
||||
|
||||
async def get_workflow_run_output_parameters(self, workflow_run_id: str) -> list[WorkflowRunOutputParameter]:
|
||||
try:
|
||||
async with self.Session() as session:
|
||||
|
||||
@@ -34,6 +34,7 @@ AWS_SECRET_PARAMETER_PREFIX = "asp"
|
||||
BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX = "bccd"
|
||||
BITWARDEN_LOGIN_CREDENTIAL_PARAMETER_PREFIX = "blc"
|
||||
BITWARDEN_SENSITIVE_INFORMATION_PARAMETER_PREFIX = "bsi"
|
||||
CREDENTIAL_ONEPASSWORD_PARAMETER_PREFIX = "opp"
|
||||
CREDENTIAL_PARAMETER_PREFIX = "cp"
|
||||
CREDENTIAL_PREFIX = "cred"
|
||||
ORGANIZATION_BITWARDEN_COLLECTION_PREFIX = "obc"
|
||||
@@ -106,6 +107,11 @@ def generate_bitwarden_credit_card_data_parameter_id() -> str:
|
||||
return f"{BITWARDEN_CREDIT_CARD_DATA_PARAMETER_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_onepassword_credential_parameter_id() -> str:
|
||||
int_id = generate_id()
|
||||
return f"{CREDENTIAL_ONEPASSWORD_PARAMETER_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_organization_auth_token_id() -> str:
|
||||
int_id = generate_id()
|
||||
return f"{ORGANIZATION_AUTH_TOKEN_PREFIX}_{int_id}"
|
||||
|
||||
@@ -29,6 +29,7 @@ from skyvern.forge.sdk.db.id import (
|
||||
generate_bitwarden_sensitive_information_parameter_id,
|
||||
generate_credential_id,
|
||||
generate_credential_parameter_id,
|
||||
generate_onepassword_credential_parameter_id,
|
||||
generate_org_id,
|
||||
generate_organization_auth_token_id,
|
||||
generate_organization_bitwarden_collection_id,
|
||||
@@ -416,6 +417,28 @@ class CredentialParameterModel(Base):
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
class OnePasswordCredentialParameterModel(Base):
|
||||
__tablename__ = "onepassword_credential_parameters"
|
||||
|
||||
onepassword_credential_parameter_id = Column(
|
||||
String, primary_key=True, index=True, default=generate_onepassword_credential_parameter_id
|
||||
)
|
||||
workflow_id = Column(String, index=True, nullable=False)
|
||||
key = Column(String, nullable=False)
|
||||
description = Column(String, nullable=True)
|
||||
vault_id = Column(String, nullable=False)
|
||||
item_id = Column(String, nullable=False)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(
|
||||
DateTime,
|
||||
default=datetime.datetime.utcnow,
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
deleted_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
class WorkflowRunParameterModel(Base):
|
||||
__tablename__ = "workflow_run_parameters"
|
||||
|
||||
|
||||
191
skyvern/forge/sdk/services/credentials.py
Normal file
191
skyvern/forge/sdk/services/credentials.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import json
|
||||
import logging
|
||||
from enum import StrEnum
|
||||
from typing import Optional
|
||||
|
||||
from onepassword.client import Client as OnePasswordClient
|
||||
|
||||
from skyvern.config import settings
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OnePasswordConstants(StrEnum):
|
||||
"""Constants for 1Password integration."""
|
||||
|
||||
TOTP = "OP_TOTP" # Special value to indicate a TOTP code
|
||||
|
||||
|
||||
async def resolve_secret(vault_id: str, item_id: str) -> str:
|
||||
"""
|
||||
Resolve a 1Password secret using vault_id and item_id directly.
|
||||
|
||||
Args:
|
||||
vault_id: The 1Password vault ID
|
||||
item_id: The 1Password item ID
|
||||
|
||||
Returns:
|
||||
The resolved secret value
|
||||
"""
|
||||
token = settings.OP_SERVICE_ACCOUNT_TOKEN
|
||||
if not token:
|
||||
raise ValueError("OP_SERVICE_ACCOUNT_TOKEN not configured in settings")
|
||||
|
||||
client = await OnePasswordClient.authenticate(
|
||||
auth=token,
|
||||
integration_name="Skyvern 1Password",
|
||||
integration_version="v1.0.0",
|
||||
)
|
||||
|
||||
result = await get_1password_item_details(client, vault_id, item_id)
|
||||
return result
|
||||
|
||||
|
||||
async def get_1password_item_details(client: OnePasswordClient, vault_id: str, item_id: str) -> str:
|
||||
"""
|
||||
Get details of a 1Password item.
|
||||
|
||||
Args:
|
||||
client: Authenticated 1Password client
|
||||
vault_id: The vault ID
|
||||
item_id: The item ID
|
||||
|
||||
Returns:
|
||||
JSON string containing item fields and their values
|
||||
"""
|
||||
try:
|
||||
item = await client.items.get(vault_id, item_id)
|
||||
|
||||
# Check if item is None
|
||||
if item is None:
|
||||
LOG.error(f"No item found for vault_id:{vault_id}, item_id:{item_id}")
|
||||
raise ValueError(f"1Password item not found: vault_id:{vault_id}, item_id:{item_id}")
|
||||
|
||||
# Create a dictionary of all fields
|
||||
result = {}
|
||||
|
||||
# Debug: Log the structure of the item and fields
|
||||
LOG.info(
|
||||
f"1Password item structure: {dir(item)}"
|
||||
+ (f"\nFirst field structure: {dir(item.fields[0])}" if hasattr(item, "fields") and item.fields else "")
|
||||
)
|
||||
# We don't log field values as they may contain sensitive credentials
|
||||
|
||||
# Add all fields with proper attribute checking
|
||||
for i, field in enumerate(item.fields):
|
||||
# Debug: Log each field's structure
|
||||
LOG.debug(f"Field {i} structure: {dir(field)}")
|
||||
|
||||
if hasattr(field, "value") and field.value is not None:
|
||||
# Safely get field identifier - use id attribute or fallback to a default
|
||||
try:
|
||||
# Try different possible attribute names for the field identifier
|
||||
field_id = None
|
||||
|
||||
# Check all available attributes on the field object
|
||||
field_attrs = dir(field)
|
||||
LOG.debug(f"Field {i} attributes: {field_attrs}")
|
||||
|
||||
# Try to get the most appropriate identifier
|
||||
if hasattr(field, "id") and field.id:
|
||||
field_id = field.id
|
||||
LOG.debug(f"Using field.id: {field_id}")
|
||||
elif hasattr(field, "name") and field.name:
|
||||
field_id = field.name
|
||||
LOG.debug(f"Using field.name: {field_id}")
|
||||
elif hasattr(field, "label") and field.label:
|
||||
field_id = field.label
|
||||
LOG.debug(f"Using field.label: {field_id}")
|
||||
elif hasattr(field, "type") and field.type:
|
||||
field_id = f"{field.type}_{i}"
|
||||
LOG.debug(f"Using field.type: {field_id}")
|
||||
else:
|
||||
# If no identifier found, generate one based on index
|
||||
field_id = f"field_{i}"
|
||||
LOG.debug(f"Using generated id: {field_id}")
|
||||
|
||||
# Create a safe key name
|
||||
key = str(field_id).lower().replace(" ", "_")
|
||||
result[key] = field.value
|
||||
LOG.debug(f"Added field with key '{key}' and value type: {type(field.value).__name__}")
|
||||
|
||||
except Exception as field_err:
|
||||
LOG.warning(f"Error processing field {i}: {field_err}")
|
||||
# Still try to capture the value with a generic key
|
||||
result[f"field_{i}"] = field.value
|
||||
|
||||
# Explicitly look for username and password fields
|
||||
for i, field in enumerate(item.fields):
|
||||
try:
|
||||
# Check for username field using various possible attributes
|
||||
if "username" not in result:
|
||||
if hasattr(field, "id") and field.id == "username" and hasattr(field, "value") and field.value:
|
||||
result["username"] = field.value
|
||||
LOG.debug(f"Found username field at index {i}")
|
||||
elif (
|
||||
hasattr(field, "purpose")
|
||||
and field.purpose == "USERNAME"
|
||||
and hasattr(field, "value")
|
||||
and field.value
|
||||
):
|
||||
result["username"] = field.value
|
||||
LOG.debug(f"Found username field by purpose at index {i}")
|
||||
elif (
|
||||
hasattr(field, "type") and field.type == "USERNAME" and hasattr(field, "value") and field.value
|
||||
):
|
||||
result["username"] = field.value
|
||||
LOG.debug(f"Found username field by type at index {i}")
|
||||
|
||||
# Check for password field using various possible attributes
|
||||
if "password" not in result:
|
||||
if hasattr(field, "id") and field.id == "password" and hasattr(field, "value") and field.value:
|
||||
result["password"] = field.value
|
||||
LOG.debug(f"Found password field at index {i}")
|
||||
elif (
|
||||
hasattr(field, "purpose")
|
||||
and field.purpose == "PASSWORD"
|
||||
and hasattr(field, "value")
|
||||
and field.value
|
||||
):
|
||||
result["password"] = field.value
|
||||
LOG.debug(f"Found password field by purpose at index {i}")
|
||||
elif (
|
||||
hasattr(field, "type") and field.type == "PASSWORD" and hasattr(field, "value") and field.value
|
||||
):
|
||||
result["password"] = field.value
|
||||
LOG.debug(f"Found password field by type at index {i}")
|
||||
except Exception as field_err:
|
||||
LOG.warning(f"Error processing username/password field at index {i}: {field_err}")
|
||||
|
||||
# Add TOTP if available
|
||||
try:
|
||||
totp = await get_totp_for_item(client, vault_id, item_id)
|
||||
if totp:
|
||||
result["totp"] = totp
|
||||
except Exception as totp_err:
|
||||
LOG.warning(f"Error getting TOTP: {totp_err}")
|
||||
|
||||
return json.dumps(result)
|
||||
except Exception as e:
|
||||
LOG.error(f"Error retrieving 1Password item {vault_id}:{item_id}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
async def get_totp_for_item(client: OnePasswordClient, vault_id: str, item_id: str) -> Optional[str]:
|
||||
"""
|
||||
Get the TOTP code for a 1Password item if available.
|
||||
|
||||
Args:
|
||||
client: Authenticated 1Password client
|
||||
vault_id: The vault ID
|
||||
item_id: The item ID
|
||||
|
||||
Returns:
|
||||
TOTP code if available, None otherwise
|
||||
"""
|
||||
try:
|
||||
totp = await client.items.get_totp(vault_id, item_id)
|
||||
return totp
|
||||
except Exception:
|
||||
# TOTP might not be available for this item
|
||||
return None
|
||||
@@ -1,8 +1,10 @@
|
||||
import copy
|
||||
import json
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
import structlog
|
||||
from onepassword.client import Client as OnePasswordClient
|
||||
|
||||
from skyvern.config import settings
|
||||
from skyvern.exceptions import (
|
||||
@@ -17,6 +19,7 @@ from skyvern.forge.sdk.schemas.credentials import PasswordCredential
|
||||
from skyvern.forge.sdk.schemas.organizations import Organization
|
||||
from skyvern.forge.sdk.schemas.tasks import TaskStatus
|
||||
from skyvern.forge.sdk.services.bitwarden import BitwardenConstants, BitwardenService
|
||||
from skyvern.forge.sdk.services.credentials import OnePasswordConstants, resolve_secret
|
||||
from skyvern.forge.sdk.workflow.exceptions import OutputParameterKeyCollisionError
|
||||
from skyvern.forge.sdk.workflow.models.parameter import (
|
||||
PARAMETER_TYPE,
|
||||
@@ -26,6 +29,7 @@ from skyvern.forge.sdk.workflow.models.parameter import (
|
||||
BitwardenSensitiveInformationParameter,
|
||||
ContextParameter,
|
||||
CredentialParameter,
|
||||
OnePasswordCredentialParameter,
|
||||
OutputParameter,
|
||||
Parameter,
|
||||
ParameterType,
|
||||
@@ -86,6 +90,8 @@ class WorkflowRunContext:
|
||||
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(secrete_parameter, organization)
|
||||
elif isinstance(secrete_parameter, OnePasswordCredentialParameter):
|
||||
await workflow_run_context.register_onepassword_credential_parameter_value(secrete_parameter)
|
||||
elif isinstance(secrete_parameter, BitwardenLoginCredentialParameter):
|
||||
await workflow_run_context.register_bitwarden_login_credential_parameter_value(
|
||||
aws_client, secrete_parameter, organization
|
||||
@@ -180,6 +186,29 @@ class WorkflowRunContext:
|
||||
def generate_random_secret_id() -> str:
|
||||
return f"secret_{uuid.uuid4()}"
|
||||
|
||||
async def _get_credential_vault_and_item_ids(self, credential_id: str) -> tuple[str, str]:
|
||||
"""
|
||||
Extract vault_id and item_id from the credential_id.
|
||||
This method handles the legacy format vault_id:item_id.
|
||||
|
||||
Args:
|
||||
credential_id: The credential identifier in the format vault_id:item_id
|
||||
|
||||
Returns:
|
||||
A tuple of (vault_id, item_id)
|
||||
|
||||
Raises:
|
||||
ValueError: If the credential format is invalid
|
||||
"""
|
||||
# Check if it's in the format vault_id:item_id
|
||||
if ":" in credential_id:
|
||||
LOG.info(f"Processing credential in vault_id:item_id format: {credential_id}")
|
||||
vault_id, item_id = credential_id.split(":", 1)
|
||||
return vault_id, item_id
|
||||
|
||||
# If we can't parse the credential_id, raise an error
|
||||
raise ValueError(f"Invalid credential format: {credential_id}. Expected format: vault_id:item_id")
|
||||
|
||||
async def register_secret_workflow_parameter_value(
|
||||
self,
|
||||
parameter: WorkflowParameter,
|
||||
@@ -195,30 +224,108 @@ class WorkflowRunContext:
|
||||
|
||||
LOG.info(f"Fetching credential parameter value for credential: {credential_id}")
|
||||
|
||||
db_credential = await app.DATABASE.get_credential(credential_id, organization_id=organization.organization_id)
|
||||
if db_credential is None:
|
||||
raise CredentialParameterNotFoundError(credential_id)
|
||||
try:
|
||||
# Extract vault_id and item_id from the database
|
||||
vault_id, item_id = await self._get_credential_vault_and_item_ids(credential_id)
|
||||
|
||||
bitwarden_credential = await BitwardenService.get_credential_item(db_credential.item_id)
|
||||
# Use the 1Password SDK to resolve the reference using vault_id and item_id directly
|
||||
secret_value_json = await resolve_secret(vault_id, item_id)
|
||||
|
||||
credential_item = bitwarden_credential.credential
|
||||
# Validate the JSON response
|
||||
if not secret_value_json:
|
||||
LOG.error(f"Empty response from 1Password for credential: {credential_id}")
|
||||
raise ValueError(f"Empty response from 1Password for credential: {credential_id}")
|
||||
|
||||
self.parameters[parameter.key] = parameter
|
||||
self.values[parameter.key] = {}
|
||||
credential_dict = credential_item.model_dump()
|
||||
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
|
||||
self.values[parameter.key][key] = secret_id
|
||||
try:
|
||||
secret_values = json.loads(secret_value_json)
|
||||
except json.JSONDecodeError as json_err:
|
||||
LOG.error(f"Invalid JSON response from 1Password: {secret_value_json[:100]}... Error: {json_err}")
|
||||
raise ValueError(f"Invalid JSON response from 1Password: {json_err}")
|
||||
|
||||
if isinstance(credential_item, PasswordCredential) and credential_item.totp is not None:
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
totp_secret_id = f"{random_secret_id}_totp"
|
||||
self.secrets[totp_secret_id] = BitwardenConstants.TOTP
|
||||
totp_secret_value = self.totp_secret_value_key(totp_secret_id)
|
||||
self.secrets[totp_secret_value] = credential_item.totp
|
||||
self.values[parameter.key]["totp"] = totp_secret_id
|
||||
if not secret_values:
|
||||
LOG.warning(f"No values found in 1Password item: {credential_id}")
|
||||
# Still continue with empty values
|
||||
|
||||
self.parameters[parameter.key] = parameter
|
||||
self.values[parameter.key] = {}
|
||||
|
||||
# Process fields from the 1Password item
|
||||
if "fields" in secret_values and isinstance(secret_values["fields"], list):
|
||||
for field in secret_values["fields"]:
|
||||
if not isinstance(field, dict) or "id" not in field or "value" not in field:
|
||||
continue
|
||||
|
||||
field_id = field.get("id")
|
||||
field_type = field.get("field_type")
|
||||
field_value = field.get("value")
|
||||
|
||||
# Store the field value
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
secret_id = f"{random_secret_id}_{field_id}"
|
||||
self.secrets[secret_id] = field_value
|
||||
self.values[parameter.key][field_id] = secret_id
|
||||
|
||||
# For TOTP fields, also store the current code
|
||||
if field_type == "Totp" and isinstance(field.get("details"), dict):
|
||||
details = field.get("details")
|
||||
# Explicitly check that details is a dict before accessing get method
|
||||
if isinstance(details, dict):
|
||||
content = details.get("content")
|
||||
if isinstance(content, dict) and "code" in content:
|
||||
totp_code = content["code"]
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
totp_secret_id = f"{random_secret_id}_totp"
|
||||
self.secrets[totp_secret_id] = totp_code
|
||||
totp_secret_value = self.totp_secret_value_key(totp_secret_id)
|
||||
self.secrets[totp_secret_value] = field_value # Store the TOTP secret
|
||||
self.values[parameter.key]["totp"] = totp_secret_id
|
||||
else:
|
||||
# Process each field in the 1Password item (old format or custom format)
|
||||
for key, value in secret_values.items():
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
secret_id = f"{random_secret_id}_{key}"
|
||||
self.secrets[secret_id] = value
|
||||
self.values[parameter.key][key] = secret_id
|
||||
|
||||
LOG.info("Successfully processed 1Password credential")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to process 1Password credential: {credential_id}. Error: {str(e)}")
|
||||
# Add more context to the error
|
||||
raise ValueError(f"Failed to process 1Password credential {credential_id}: {str(e)}") from e
|
||||
|
||||
# Handle regular credentials from the database
|
||||
try:
|
||||
db_credential = await app.DATABASE.get_credential(
|
||||
credential_id, organization_id=organization.organization_id
|
||||
)
|
||||
if db_credential is None:
|
||||
raise CredentialParameterNotFoundError(credential_id)
|
||||
|
||||
bitwarden_credential = await BitwardenService.get_credential_item(db_credential.item_id)
|
||||
|
||||
credential_item = bitwarden_credential.credential
|
||||
|
||||
self.parameters[parameter.key] = parameter
|
||||
self.values[parameter.key] = {}
|
||||
credential_dict = credential_item.model_dump()
|
||||
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
|
||||
self.values[parameter.key][key] = secret_id
|
||||
|
||||
if isinstance(credential_item, PasswordCredential) and credential_item.totp is not None:
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
totp_secret_id = f"{random_secret_id}_totp"
|
||||
self.secrets[totp_secret_id] = BitwardenConstants.TOTP
|
||||
totp_secret_value = self.totp_secret_value_key(totp_secret_id)
|
||||
self.secrets[totp_secret_value] = credential_item.totp
|
||||
self.values[parameter.key]["totp"] = totp_secret_id
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to get credential from database: {credential_id}. Error: {e}")
|
||||
raise e
|
||||
|
||||
async def register_credential_parameter_value(
|
||||
self,
|
||||
@@ -278,6 +385,56 @@ class WorkflowRunContext:
|
||||
self.values[parameter.key] = random_secret_id
|
||||
self.parameters[parameter.key] = parameter
|
||||
|
||||
async def register_onepassword_credential_parameter_value(self, parameter: OnePasswordCredentialParameter) -> None:
|
||||
token = settings.OP_SERVICE_ACCOUNT_TOKEN
|
||||
if not token:
|
||||
raise ValueError("OP_SERVICE_ACCOUNT_TOKEN environment variable not set")
|
||||
|
||||
client = await OnePasswordClient.authenticate(
|
||||
auth=token,
|
||||
integration_name="Skyvern",
|
||||
integration_version="v1.0.0",
|
||||
)
|
||||
|
||||
item = await client.items.get(parameter.vault_id, parameter.item_id)
|
||||
|
||||
# Check if item is None
|
||||
if item is None:
|
||||
LOG.error(f"No item found for vault_id:{parameter.vault_id}, item_id:{parameter.item_id}")
|
||||
raise ValueError(f"1Password item not found: vault_id:{parameter.vault_id}, item_id:{parameter.item_id}")
|
||||
|
||||
self.parameters[parameter.key] = parameter
|
||||
self.values[parameter.key] = {}
|
||||
|
||||
# Process all fields
|
||||
for field in item.fields:
|
||||
if field.value is None:
|
||||
continue
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
secret_id = f"{random_secret_id}_{field.id}"
|
||||
self.secrets[secret_id] = field.value
|
||||
key = (field.label or field.id).lower().replace(" ", "_")
|
||||
self.values[parameter.key][key] = secret_id
|
||||
|
||||
# Try to get TOTP if available
|
||||
try:
|
||||
totp = await client.items.get_totp(parameter.vault_id, parameter.item_id)
|
||||
if totp:
|
||||
# Store the actual TOTP value in a separate secret for internal use
|
||||
random_secret_id = self.generate_random_secret_id()
|
||||
totp_value_id = f"{random_secret_id}_totp_value"
|
||||
self.secrets[totp_value_id] = totp
|
||||
|
||||
# Store the special TOTP constant that the agent will recognize
|
||||
totp_secret_id = f"{random_secret_id}_totp"
|
||||
self.secrets[totp_secret_id] = OnePasswordConstants.TOTP
|
||||
self.values[parameter.key]["totp"] = totp_secret_id
|
||||
|
||||
LOG.info(f"TOTP code available for item {parameter.item_id}")
|
||||
except Exception as e:
|
||||
# TOTP might not be available for this item, just log and continue
|
||||
LOG.debug(f"TOTP not available for item {parameter.item_id}: {str(e)}")
|
||||
|
||||
async def register_bitwarden_login_credential_parameter_value(
|
||||
self,
|
||||
aws_client: AsyncAWSClient,
|
||||
|
||||
@@ -18,6 +18,7 @@ class ParameterType(StrEnum):
|
||||
BITWARDEN_LOGIN_CREDENTIAL = "bitwarden_login_credential"
|
||||
BITWARDEN_SENSITIVE_INFORMATION = "bitwarden_sensitive_information"
|
||||
BITWARDEN_CREDIT_CARD_DATA = "bitwarden_credit_card_data"
|
||||
ONEPASSWORD = "onepassword"
|
||||
OUTPUT = "output"
|
||||
CREDENTIAL = "credential"
|
||||
|
||||
@@ -127,6 +128,19 @@ class BitwardenCreditCardDataParameter(Parameter):
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class OnePasswordCredentialParameter(Parameter):
|
||||
parameter_type: Literal[ParameterType.ONEPASSWORD] = ParameterType.ONEPASSWORD
|
||||
|
||||
onepassword_credential_parameter_id: str
|
||||
workflow_id: str
|
||||
vault_id: str
|
||||
item_id: str
|
||||
|
||||
created_at: datetime
|
||||
modified_at: datetime
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class WorkflowParameterType(StrEnum):
|
||||
STRING = "string"
|
||||
INTEGER = "integer"
|
||||
@@ -203,6 +217,7 @@ ParameterSubclasses = Union[
|
||||
BitwardenLoginCredentialParameter,
|
||||
BitwardenSensitiveInformationParameter,
|
||||
BitwardenCreditCardDataParameter,
|
||||
OnePasswordCredentialParameter,
|
||||
OutputParameter,
|
||||
CredentialParameter,
|
||||
]
|
||||
|
||||
@@ -86,6 +86,12 @@ class BitwardenCreditCardDataParameterYAML(ParameterYAML):
|
||||
bitwarden_item_id: str
|
||||
|
||||
|
||||
class OnePasswordCredentialParameterYAML(ParameterYAML):
|
||||
parameter_type: Literal[ParameterType.ONEPASSWORD] = ParameterType.ONEPASSWORD # type: ignore
|
||||
vault_id: str
|
||||
item_id: str
|
||||
|
||||
|
||||
class WorkflowParameterYAML(ParameterYAML):
|
||||
# There is a mypy bug with Literal. Without the type: ignore, mypy will raise an error:
|
||||
# Parameter 1 of Literal[...] cannot be of type "Any"
|
||||
@@ -370,6 +376,7 @@ PARAMETER_YAML_SUBCLASSES = (
|
||||
| BitwardenLoginCredentialParameterYAML
|
||||
| BitwardenSensitiveInformationParameterYAML
|
||||
| BitwardenCreditCardDataParameterYAML
|
||||
| OnePasswordCredentialParameterYAML
|
||||
| WorkflowParameterYAML
|
||||
| ContextParameterYAML
|
||||
| OutputParameterYAML
|
||||
|
||||
@@ -68,6 +68,7 @@ from skyvern.forge.sdk.workflow.models.parameter import (
|
||||
BitwardenSensitiveInformationParameter,
|
||||
ContextParameter,
|
||||
CredentialParameter,
|
||||
OnePasswordCredentialParameter,
|
||||
OutputParameter,
|
||||
Parameter,
|
||||
ParameterType,
|
||||
@@ -239,6 +240,7 @@ class WorkflowService:
|
||||
BitwardenLoginCredentialParameter,
|
||||
BitwardenCreditCardDataParameter,
|
||||
BitwardenSensitiveInformationParameter,
|
||||
OnePasswordCredentialParameter,
|
||||
CredentialParameter,
|
||||
),
|
||||
)
|
||||
@@ -883,6 +885,22 @@ class WorkflowService:
|
||||
description=description,
|
||||
)
|
||||
|
||||
async def create_onepassword_credential_parameter(
|
||||
self,
|
||||
workflow_id: str,
|
||||
key: str,
|
||||
vault_id: str,
|
||||
item_id: str,
|
||||
description: str | None = None,
|
||||
) -> OnePasswordCredentialParameter:
|
||||
return await app.DATABASE.create_onepassword_credential_parameter(
|
||||
workflow_id=workflow_id,
|
||||
key=key,
|
||||
vault_id=vault_id,
|
||||
item_id=item_id,
|
||||
description=description,
|
||||
)
|
||||
|
||||
async def create_bitwarden_sensitive_information_parameter(
|
||||
self,
|
||||
workflow_id: str,
|
||||
@@ -1490,6 +1508,14 @@ class WorkflowService:
|
||||
description=parameter.description,
|
||||
credential_id=parameter.credential_id,
|
||||
)
|
||||
elif parameter.parameter_type == ParameterType.ONEPASSWORD:
|
||||
parameters[parameter.key] = await self.create_onepassword_credential_parameter(
|
||||
workflow_id=workflow.workflow_id,
|
||||
key=parameter.key,
|
||||
description=parameter.description,
|
||||
vault_id=parameter.vault_id,
|
||||
item_id=parameter.item_id,
|
||||
)
|
||||
elif parameter.parameter_type == ParameterType.BITWARDEN_LOGIN_CREDENTIAL:
|
||||
if not parameter.bitwarden_collection_id and not parameter.bitwarden_item_id:
|
||||
raise WorkflowParameterMissingRequiredValue(
|
||||
|
||||
Reference in New Issue
Block a user