Support credit cards in 1Password credential parameters (#3746)

This commit is contained in:
Stanislav Novosad
2025-10-17 10:13:47 -06:00
committed by GitHub
parent e3ecc4b657
commit 75ce98e841
3 changed files with 87 additions and 12 deletions

View File

@@ -25,6 +25,7 @@ import { WorkflowParameterInput } from "../../WorkflowParameterInput";
import { ParametersState } from "../types"; import { ParametersState } from "../types";
import { getDefaultValueForParameterType } from "../workflowEditorUtils"; import { getDefaultValueForParameterType } from "../workflowEditorUtils";
import { validateBitwardenLoginCredential } from "./util"; import { validateBitwardenLoginCredential } from "./util";
import { HelpTooltip } from "@/components/HelpTooltip";
type Props = { type Props = {
type: WorkflowEditorParameterType; type: WorkflowEditorParameterType;
@@ -273,19 +274,32 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
{type === "credential" && credentialType === "onepassword" && ( {type === "credential" && credentialType === "onepassword" && (
<> <>
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs text-slate-300">Vault ID</Label> <div className="flex gap-2">
<Label className="text-xs text-slate-300">Vault ID</Label>
<HelpTooltip content="You can find the Vault ID and Item ID in the URL when viewing the item in 1Password on the web." />
</div>
<Input <Input
value={vaultId} value={vaultId}
onChange={(e) => setVaultId(e.target.value)} onChange={(e) => setVaultId(e.target.value)}
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs text-slate-300">Item ID</Label> <div className="flex gap-2">
<Label className="text-xs text-slate-300">Item ID</Label>
<HelpTooltip content="Supports all 1Password item types: Logins, Passwords, Credit Cards, Secure Notes, and more." />
</div>
<Input <Input
value={itemId} value={itemId}
onChange={(e) => setItemId(e.target.value)} onChange={(e) => setItemId(e.target.value)}
/> />
</div> </div>
<div className="rounded-md bg-slate-800 p-2">
<div className="space-y-1 text-xs text-slate-400">
* Credit Cards: Due to a 1Password limitation, add the
expiration date as a separate text field named "Expire Date"
in the format MM/YYYY (e.g. 09/2027).
</div>
</div>
</> </>
)} )}
{type === "credential" && credentialType === "azurevault" && ( {type === "credential" && credentialType === "azurevault" && (

View File

@@ -31,6 +31,7 @@ import {
} from "../types"; } from "../types";
import { getDefaultValueForParameterType } from "../workflowEditorUtils"; import { getDefaultValueForParameterType } from "../workflowEditorUtils";
import { validateBitwardenLoginCredential } from "./util"; import { validateBitwardenLoginCredential } from "./util";
import { HelpTooltip } from "@/components/HelpTooltip";
type Props = { type Props = {
type: WorkflowEditorParameterType; type: WorkflowEditorParameterType;
@@ -357,19 +358,32 @@ function WorkflowParameterEditPanel({
{type === "credential" && credentialType === "onepassword" && ( {type === "credential" && credentialType === "onepassword" && (
<> <>
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs text-slate-300">Vault ID</Label> <div className="flex gap-2">
<Label className="text-xs text-slate-300">Vault ID</Label>
<HelpTooltip content="You can find the Vault ID and Item ID in the URL when viewing the item in 1Password on the web." />
</div>
<Input <Input
value={vaultId} value={vaultId}
onChange={(e) => setVaultId(e.target.value)} onChange={(e) => setVaultId(e.target.value)}
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs text-slate-300">Item ID</Label> <div className="flex gap-2">
<Label className="text-xs text-slate-300">Item ID</Label>
<HelpTooltip content="Supports all 1Password item types: Logins, Passwords, Credit Cards, Secure Notes, and more." />
</div>
<Input <Input
value={opItemId} value={opItemId}
onChange={(e) => setOpItemId(e.target.value)} onChange={(e) => setOpItemId(e.target.value)}
/> />
</div> </div>
<div className="rounded-md bg-slate-800 p-2">
<div className="space-y-1 text-xs text-slate-400">
Credit Cards: Due to a 1Password limitation, add the
expiration date as a separate text field named Expire Date
in the format MM/YYYY (e.g. 09/2027).
</div>
</div>
</> </>
)} )}
{type === "credential" && credentialType === "azurevault" && ( {type === "credential" && credentialType === "azurevault" && (

View File

@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Self
import structlog import structlog
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
from onepassword import ItemFieldType
from onepassword.client import Client as OnePasswordClient from onepassword.client import Client as OnePasswordClient
from skyvern.config import settings from skyvern.config import settings
@@ -452,24 +453,61 @@ class WorkflowRunContext:
"context": "These values are placeholders. When you type this in, the real value gets inserted (For security reasons)", "context": "These values are placeholders. When you type this in, the real value gets inserted (For security reasons)",
} }
# Process all fields # Process all fields generically so it covers passwords and credit cards
for field in item.fields: for field in item.fields:
if field.value is None: if not field.value or field.field_type == ItemFieldType.UNSUPPORTED:
continue continue
random_secret_id = self.generate_random_secret_id()
# ignore irrelevant fields to avoid confusing AI
if field.id in ["validFrom", "interest", "issuenumber"]:
continue
field_type = field.field_type.value.lower() field_type = field.field_type.value.lower()
if field_type == "totp": if field_type == "totp":
random_secret_id = self.generate_random_secret_id()
totp_secret_id = f"{random_secret_id}_totp" totp_secret_id = f"{random_secret_id}_totp"
self.secrets[totp_secret_id] = OnePasswordConstants.TOTP self.secrets[totp_secret_id] = OnePasswordConstants.TOTP
totp_secret_value = self.totp_secret_value_key(totp_secret_id) totp_secret_value = self.totp_secret_value_key(totp_secret_id)
self.secrets[totp_secret_value] = parse_totp_secret(field.value) self.secrets[totp_secret_value] = parse_totp_secret(field.value)
self.values[parameter.key]["totp"] = totp_secret_id self.values[parameter.key]["totp"] = totp_secret_id
elif field.title and field.title.lower() in ["expire date", "expiry date", "expiration date"]:
parts = [part.strip() for part in field.value.strip().split("/")]
if len(parts) == 2:
month, year_part = parts
month = month.zfill(2) # ensure '5' becomes '05'
if len(year_part) == 4:
year = year_part[2:] # 2025 -> 25
else:
year = year_part
self._add_secret_parameter_value(parameter, "card_exp_month", month)
self._add_secret_parameter_value(parameter, "card_exp_year", year)
if len(year) == 2:
self._add_secret_parameter_value(parameter, "card_exp_mmyy", f"{month}/{year}")
self._add_secret_parameter_value(parameter, "card_exp_mmyyyy", f"{month}/20{year}")
else:
# store the 1password-provided value additionally
self._add_secret_parameter_value(parameter, "card_exp", field.value)
else:
# fallback on the 1password-provided value
self._add_secret_parameter_value(parameter, "card_exp", field.value)
else: else:
# this will be the username or password or other field # using more descriptive keys than 1password provides by default
key = field.id.replace(" ", "_") if field.id == "ccnum":
secret_id = f"{random_secret_id}_{key}" self._add_secret_parameter_value(parameter, "card_number", field.value)
self.secrets[secret_id] = field.value elif field.id == "cardholder":
self.values[parameter.key][key] = secret_id self._add_secret_parameter_value(parameter, "card_holder_name", field.value)
elif field.id == "cvv":
self._add_secret_parameter_value(parameter, "card_cvv", field.value)
else:
# this will be the username, password or other fields
self._add_secret_parameter_value(parameter, field.id.replace(" ", "_"), field.value)
# Secure Note support
if item.notes:
self._add_secret_parameter_value(parameter, "notes", item.notes)
async def register_bitwarden_login_credential_parameter_value( async def register_bitwarden_login_credential_parameter_value(
self, self,
@@ -981,6 +1019,15 @@ class WorkflowRunContext:
azure_vault_client = AsyncAzureVaultClient.create_default() azure_vault_client = AsyncAzureVaultClient.create_default()
return azure_vault_client return azure_vault_client
def _add_secret_parameter_value(self, parameter: Parameter, key: str, value: str) -> None:
if parameter.key not in self.values:
raise ValueError(f"{parameter.key} is missing")
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
class WorkflowContextManager: class WorkflowContextManager:
aws_client: AsyncAWSClient aws_client: AsyncAWSClient