Additional changes for proper templating and masked storage of new Secret Credential type (#4253)
This commit is contained in:
@@ -9,6 +9,7 @@ from skyvern.forge.sdk.schemas.credentials import (
|
|||||||
CredentialType,
|
CredentialType,
|
||||||
CreditCardCredential,
|
CreditCardCredential,
|
||||||
PasswordCredential,
|
PasswordCredential,
|
||||||
|
SecretCredential,
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG = structlog.get_logger()
|
LOG = structlog.get_logger()
|
||||||
@@ -35,7 +36,9 @@ class CustomCredentialAPIClient:
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _credential_to_api_payload(self, credential: PasswordCredential | CreditCardCredential) -> dict[str, Any]:
|
def _credential_to_api_payload(
|
||||||
|
self, credential: PasswordCredential | CreditCardCredential | SecretCredential
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Convert Skyvern credential to API payload format."""
|
"""Convert Skyvern credential to API payload format."""
|
||||||
if isinstance(credential, PasswordCredential):
|
if isinstance(credential, PasswordCredential):
|
||||||
return {
|
return {
|
||||||
@@ -55,6 +58,14 @@ class CustomCredentialAPIClient:
|
|||||||
"card_cvv": credential.card_cvv,
|
"card_cvv": credential.card_cvv,
|
||||||
"card_brand": credential.card_brand,
|
"card_brand": credential.card_brand,
|
||||||
}
|
}
|
||||||
|
elif isinstance(credential, SecretCredential):
|
||||||
|
payload = {
|
||||||
|
"type": "secret",
|
||||||
|
"secret_value": credential.secret_value,
|
||||||
|
}
|
||||||
|
if credential.secret_label is not None:
|
||||||
|
payload["secret_label"] = credential.secret_label
|
||||||
|
return payload
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Unsupported credential type: {type(credential)}")
|
raise TypeError(f"Unsupported credential type: {type(credential)}")
|
||||||
|
|
||||||
@@ -107,10 +118,28 @@ class CustomCredentialAPIClient:
|
|||||||
name=name,
|
name=name,
|
||||||
credential_type=CredentialType.CREDIT_CARD,
|
credential_type=CredentialType.CREDIT_CARD,
|
||||||
)
|
)
|
||||||
|
elif credential_type == "secret":
|
||||||
|
required_fields = ["secret_value"]
|
||||||
|
missing = [f for f in required_fields if f not in credential_data]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Missing required secret fields from API: {missing}")
|
||||||
|
|
||||||
|
credential = SecretCredential(
|
||||||
|
secret_value=credential_data["secret_value"],
|
||||||
|
secret_label=credential_data.get("secret_label"),
|
||||||
|
)
|
||||||
|
return CredentialItem(
|
||||||
|
item_id=item_id,
|
||||||
|
credential=credential,
|
||||||
|
name=name,
|
||||||
|
credential_type=CredentialType.SECRET,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported credential type from API: {credential_type}")
|
raise ValueError(f"Unsupported credential type from API: {credential_type}")
|
||||||
|
|
||||||
async def create_credential(self, name: str, credential: PasswordCredential | CreditCardCredential) -> str:
|
async def create_credential(
|
||||||
|
self, name: str, credential: PasswordCredential | CreditCardCredential | SecretCredential
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Create a credential using the custom API.
|
Create a credential using the custom API.
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from skyvern.forge.sdk.schemas.credentials import (
|
|||||||
CredentialType,
|
CredentialType,
|
||||||
CreditCardCredential,
|
CreditCardCredential,
|
||||||
PasswordCredential,
|
PasswordCredential,
|
||||||
|
SecretCredential,
|
||||||
)
|
)
|
||||||
from skyvern.forge.sdk.services.credentials import parse_totp_secret
|
from skyvern.forge.sdk.services.credentials import parse_totp_secret
|
||||||
|
|
||||||
@@ -84,6 +85,26 @@ def get_list_response_item_from_bitwarden_item(item: dict) -> CredentialItem:
|
|||||||
name=item["name"],
|
name=item["name"],
|
||||||
credential_type=CredentialType.CREDIT_CARD,
|
credential_type=CredentialType.CREDIT_CARD,
|
||||||
)
|
)
|
||||||
|
elif item["type"] == BitwardenItemType.SECURE_NOTE:
|
||||||
|
notes = item.get("notes") or ""
|
||||||
|
secret_value = ""
|
||||||
|
secret_label = None
|
||||||
|
try:
|
||||||
|
parsed_notes = json.loads(notes)
|
||||||
|
if isinstance(parsed_notes, dict):
|
||||||
|
secret_value = parsed_notes.get("secret_value", "") or ""
|
||||||
|
secret_label = parsed_notes.get("secret_label")
|
||||||
|
else:
|
||||||
|
secret_value = notes
|
||||||
|
except Exception:
|
||||||
|
secret_value = notes
|
||||||
|
|
||||||
|
return CredentialItem(
|
||||||
|
item_id=item["id"],
|
||||||
|
credential=SecretCredential(secret_value=secret_value, secret_label=secret_label),
|
||||||
|
name=item["name"],
|
||||||
|
credential_type=CredentialType.SECRET,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise BitwardenGetItemError(f"Unsupported item type: {item['type']}")
|
raise BitwardenGetItemError(f"Unsupported item type: {item['type']}")
|
||||||
|
|
||||||
@@ -807,7 +828,7 @@ class BitwardenService:
|
|||||||
async def create_credential_item(
|
async def create_credential_item(
|
||||||
collection_id: str,
|
collection_id: str,
|
||||||
name: str,
|
name: str,
|
||||||
credential: PasswordCredential | CreditCardCredential,
|
credential: PasswordCredential | CreditCardCredential | SecretCredential,
|
||||||
) -> str:
|
) -> str:
|
||||||
try:
|
try:
|
||||||
master_password, bw_organization_id, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
master_password, bw_organization_id, _, _ = await BitwardenService._get_skyvern_auth_secrets()
|
||||||
@@ -820,16 +841,56 @@ class BitwardenService:
|
|||||||
name=name,
|
name=name,
|
||||||
credential=credential,
|
credential=credential,
|
||||||
)
|
)
|
||||||
else:
|
elif isinstance(credential, CreditCardCredential):
|
||||||
return await BitwardenService._create_credit_card_item_using_server(
|
return await BitwardenService._create_credit_card_item_using_server(
|
||||||
bw_organization_id=bw_organization_id,
|
bw_organization_id=bw_organization_id,
|
||||||
collection_id=collection_id,
|
collection_id=collection_id,
|
||||||
name=name,
|
name=name,
|
||||||
credential=credential,
|
credential=credential,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return await BitwardenService._create_secret_item_using_server(
|
||||||
|
bw_organization_id=bw_organization_id,
|
||||||
|
collection_id=collection_id,
|
||||||
|
name=name,
|
||||||
|
credential=credential,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _create_secret_item_using_server(
|
||||||
|
bw_organization_id: str,
|
||||||
|
collection_id: str,
|
||||||
|
name: str,
|
||||||
|
credential: SecretCredential,
|
||||||
|
) -> str:
|
||||||
|
item_template = await aiohttp_get_json(f"{BITWARDEN_SERVER_BASE_URL}/object/template/item", timeout=120)
|
||||||
|
secure_note_template = await aiohttp_get_json(
|
||||||
|
f"{BITWARDEN_SERVER_BASE_URL}/object/template/item.securenote", timeout=120
|
||||||
|
)
|
||||||
|
|
||||||
|
item_template = item_template["data"]["template"]
|
||||||
|
secure_note_template = secure_note_template["data"]["template"]
|
||||||
|
|
||||||
|
item_template["type"] = get_bitwarden_item_type_code(BitwardenItemType.SECURE_NOTE)
|
||||||
|
item_template["name"] = name
|
||||||
|
item_template["collectionIds"] = [collection_id]
|
||||||
|
item_template["organizationId"] = bw_organization_id
|
||||||
|
item_template["secureNote"] = secure_note_template
|
||||||
|
item_template["notes"] = json.dumps(
|
||||||
|
{
|
||||||
|
"secret_value": credential.secret_value,
|
||||||
|
"secret_label": credential.secret_label,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await aiohttp_post(f"{BITWARDEN_SERVER_BASE_URL}/object/item", data=item_template, timeout=120)
|
||||||
|
if not response or response.get("success") is False:
|
||||||
|
raise BitwardenCreateLoginItemError("Failed to create secret item")
|
||||||
|
|
||||||
|
return response["data"]["id"]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _get_skyvern_auth_master_password() -> str:
|
async def _get_skyvern_auth_master_password() -> str:
|
||||||
master_password = settings.SKYVERN_AUTH_BITWARDEN_MASTER_PASSWORD
|
master_password = settings.SKYVERN_AUTH_BITWARDEN_MASTER_PASSWORD
|
||||||
@@ -1011,6 +1072,27 @@ class BitwardenService:
|
|||||||
card_brand=credit_card_item["brand"],
|
card_brand=credit_card_item["brand"],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
elif response["data"]["type"] == BitwardenItemType.SECURE_NOTE:
|
||||||
|
name = response["data"]["name"]
|
||||||
|
notes = response["data"].get("notes") or ""
|
||||||
|
secret_value = ""
|
||||||
|
secret_label = None
|
||||||
|
try:
|
||||||
|
parsed_notes = json.loads(notes)
|
||||||
|
if isinstance(parsed_notes, dict):
|
||||||
|
secret_value = parsed_notes.get("secret_value", "") or ""
|
||||||
|
secret_label = parsed_notes.get("secret_label")
|
||||||
|
else:
|
||||||
|
secret_value = notes
|
||||||
|
except Exception:
|
||||||
|
secret_value = notes
|
||||||
|
|
||||||
|
return CredentialItem(
|
||||||
|
item_id=item_id,
|
||||||
|
credential_type=CredentialType.SECRET,
|
||||||
|
name=name,
|
||||||
|
credential=SecretCredential(secret_value=secret_value, secret_label=secret_label),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise BitwardenGetItemError(f"Unsupported item type: {response['data']['type']}")
|
raise BitwardenGetItemError(f"Unsupported item type: {response['data']['type']}")
|
||||||
|
|
||||||
|
|||||||
@@ -253,12 +253,13 @@ class Block(BaseModel, abc.ABC):
|
|||||||
# First collect all credential parameters to avoid modifying dict during iteration
|
# First collect all credential parameters to avoid modifying dict during iteration
|
||||||
credential_params = []
|
credential_params = []
|
||||||
for key, value in list(template_data.items()):
|
for key, value in list(template_data.items()):
|
||||||
if isinstance(value, dict) and "context" in value and "username" in value and "password" in value:
|
if isinstance(value, dict) and "context" in value:
|
||||||
credential_params.append((key, value))
|
# PASSWORD credential: has username and password
|
||||||
elif is_safe_block_for_secrets and isinstance(value, str):
|
if "username" in value and "password" in value:
|
||||||
secret_value = workflow_run_context.get_original_secret_value_or_none(value)
|
credential_params.append((key, value))
|
||||||
if secret_value is not None:
|
# SECRET credential: has secret_value
|
||||||
template_data[key] = secret_value
|
elif "secret_value" in value:
|
||||||
|
credential_params.append((key, value))
|
||||||
|
|
||||||
# Now add the real_username/real_password entries
|
# Now add the real_username/real_password entries
|
||||||
for key, value in credential_params:
|
for key, value in credential_params:
|
||||||
|
|||||||
Reference in New Issue
Block a user