From eb50fdef83d716f1f6688baae279d025a60b4728 Mon Sep 17 00:00:00 2001 From: Marc Kelechava Date: Tue, 9 Dec 2025 11:19:57 -0800 Subject: [PATCH] [Backend] Add SECRET credential type for storing generic sensitive values (#4246) --- ...12_09_1856-1faa2a5869cd_add_secrets_col.py | 31 +++++++++++++++++++ skyvern/forge/sdk/db/client.py | 2 ++ skyvern/forge/sdk/db/models.py | 1 + skyvern/forge/sdk/routes/credentials.py | 17 ++++++++++ skyvern/forge/sdk/schemas/credentials.py | 23 ++++++++++++-- .../azure_credential_vault_service.py | 24 ++++++++++++-- .../credential/credential_vault_service.py | 14 +++++++++ 7 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 alembic/versions/2025_12_09_1856-1faa2a5869cd_add_secrets_col.py diff --git a/alembic/versions/2025_12_09_1856-1faa2a5869cd_add_secrets_col.py b/alembic/versions/2025_12_09_1856-1faa2a5869cd_add_secrets_col.py new file mode 100644 index 00000000..95b12b85 --- /dev/null +++ b/alembic/versions/2025_12_09_1856-1faa2a5869cd_add_secrets_col.py @@ -0,0 +1,31 @@ +"""add secrets col + +Revision ID: 1faa2a5869cd +Revises: 135afee6e7bc +Create Date: 2025-12-09 18:56:25.666572+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "1faa2a5869cd" +down_revision: Union[str, None] = "135afee6e7bc" +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.add_column("credentials", sa.Column("secret_label", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("credentials", "secret_label") + # ### end Alembic commands ### diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index b9861c5b..4b76a644 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -4573,6 +4573,7 @@ class AgentDB: card_last4: str | None, card_brand: str | None, totp_identifier: str | None = None, + secret_label: str | None = None, ) -> Credential: async with self.Session() as session: credential = CredentialModel( @@ -4586,6 +4587,7 @@ class AgentDB: totp_identifier=totp_identifier, card_last4=card_last4, card_brand=card_brand, + secret_label=secret_label, ) session.add(credential) await session.commit() diff --git a/skyvern/forge/sdk/db/models.py b/skyvern/forge/sdk/db/models.py index 8fc44e2b..83159759 100644 --- a/skyvern/forge/sdk/db/models.py +++ b/skyvern/forge/sdk/db/models.py @@ -901,6 +901,7 @@ class CredentialModel(Base): totp_identifier = Column(String, nullable=True, default=None) card_last4 = Column(String, nullable=True) card_brand = Column(String, nullable=True) + secret_label = Column(String, nullable=True) created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False) diff --git a/skyvern/forge/sdk/routes/credentials.py b/skyvern/forge/sdk/routes/credentials.py index cb2b9d9a..bb1d0d69 100644 --- a/skyvern/forge/sdk/routes/credentials.py +++ b/skyvern/forge/sdk/routes/credentials.py @@ -29,6 +29,7 @@ from skyvern.forge.sdk.schemas.credentials import ( CredentialVaultType, CreditCardCredentialResponse, PasswordCredentialResponse, + SecretCredentialResponse, ) from skyvern.forge.sdk.schemas.organizations import ( AzureClientSecretCredentialResponse, @@ -267,6 +268,14 @@ async def create_credential( credential_type=data.credential_type, name=data.name, ) + elif data.credential_type == CredentialType.SECRET: + credential_response = SecretCredentialResponse(secret_label=data.credential.secret_label) + return CredentialResponse( + credential=credential_response, + credential_id=credential.credential_id, + credential_type=data.credential_type, + name=data.name, + ) else: raise HTTPException(status_code=400, detail=f"Unsupported credential type: {data.credential_type}") @@ -764,5 +773,13 @@ def _convert_to_response(credential: Credential) -> CredentialResponse: credential_type=credential.credential_type, name=credential.name, ) + elif credential.credential_type == CredentialType.SECRET: + credential_response = SecretCredentialResponse(secret_label=credential.secret_label) + return CredentialResponse( + credential=credential_response, + credential_id=credential.credential_id, + credential_type=credential.credential_type, + name=credential.name, + ) else: raise HTTPException(status_code=400, detail="Credential type not supported") diff --git a/skyvern/forge/sdk/schemas/credentials.py b/skyvern/forge/sdk/schemas/credentials.py index adb22338..236f2854 100644 --- a/skyvern/forge/sdk/schemas/credentials.py +++ b/skyvern/forge/sdk/schemas/credentials.py @@ -15,6 +15,7 @@ class CredentialType(StrEnum): PASSWORD = "password" CREDIT_CARD = "credit_card" + SECRET = "secret" class TotpType(StrEnum): @@ -49,6 +50,12 @@ class CreditCardCredentialResponse(BaseModel): brand: str = Field(..., description="Brand of the credit card", examples=["visa"]) +class SecretCredentialResponse(BaseModel): + """Response model for secret credentials.""" + + secret_label: str | None = Field(default=None, description="Optional label for the stored secret") + + class PasswordCredential(BaseModel): """Base model for password credentials.""" @@ -115,13 +122,22 @@ class NonEmptyCreditCardCredential(CreditCardCredential): ) +class SecretCredential(BaseModel): + """Generic secret credential.""" + + secret_value: str = Field(..., min_length=1, description="The secret value", examples=["sk-abc123"]) + secret_label: str | None = Field(default=None, description="Optional label describing the secret") + + class CredentialItem(BaseModel): """Model representing a credential item in the system.""" item_id: str = Field(..., description="Unique identifier for the credential item", examples=["cred_1234567890"]) name: str = Field(..., description="Name of the credential", examples=["Skyvern Login"]) credential_type: CredentialType = Field(..., description="Type of the credential. Eg password, credit card, etc.") - credential: PasswordCredential | CreditCardCredential = Field(..., description="The actual credential data") + credential: PasswordCredential | CreditCardCredential | SecretCredential = Field( + ..., description="The actual credential data" + ) class CreateCredentialRequest(BaseModel): @@ -129,7 +145,7 @@ class CreateCredentialRequest(BaseModel): name: str = Field(..., description="Name of the credential", examples=["Amazon Login"]) credential_type: CredentialType = Field(..., description="Type of credential to create") - credential: NonEmptyPasswordCredential | NonEmptyCreditCardCredential = Field( + credential: NonEmptyPasswordCredential | NonEmptyCreditCardCredential | SecretCredential = Field( ..., description="The credential data to store", examples=[{"username": "user@example.com", "password": "securepassword123"}], @@ -140,7 +156,7 @@ class CredentialResponse(BaseModel): """Response model for credential operations.""" credential_id: str = Field(..., description="Unique identifier for the credential", examples=["cred_1234567890"]) - credential: PasswordCredentialResponse | CreditCardCredentialResponse = Field( + credential: PasswordCredentialResponse | CreditCardCredentialResponse | SecretCredentialResponse = Field( ..., description="The credential data" ) credential_type: CredentialType = Field(..., description="Type of the credential") @@ -173,6 +189,7 @@ class Credential(BaseModel): ) card_last4: str | None = Field(..., description="For credit_card credentials: the last four digits of the card") card_brand: str | None = Field(..., description="For credit_card credentials: the card brand") + secret_label: str | None = Field(default=None, description="For secret credentials: optional label") created_at: datetime = Field(..., description="Timestamp when the credential was created") modified_at: datetime = Field(..., description="Timestamp when the credential was last modified") diff --git a/skyvern/forge/sdk/services/credential/azure_credential_vault_service.py b/skyvern/forge/sdk/services/credential/azure_credential_vault_service.py index 2b46abf7..a9cf293d 100644 --- a/skyvern/forge/sdk/services/credential/azure_credential_vault_service.py +++ b/skyvern/forge/sdk/services/credential/azure_credential_vault_service.py @@ -14,6 +14,7 @@ from skyvern.forge.sdk.schemas.credentials import ( CredentialVaultType, CreditCardCredential, PasswordCredential, + SecretCredential, ) from skyvern.forge.sdk.services.credential.credential_vault_service import CredentialVaultService @@ -36,8 +37,14 @@ class AzureCredentialVaultService(CredentialVaultService): card_brand: str card_holder_name: str + class _SecretCredentialDataImage(BaseModel): + type: Literal["secret"] + secret_value: str + secret_label: str | None = None + _CredentialDataImage = Annotated[ - Union[_PasswordCredentialDataImage, _CreditCardCredentialDataImage], Field(discriminator="type") + Union[_PasswordCredentialDataImage, _CreditCardCredentialDataImage, _SecretCredentialDataImage], + Field(discriminator="type"), ] def __init__(self, client: AsyncAzureVaultClient, vault_name: str): @@ -128,13 +135,20 @@ class AzureCredentialVaultService(CredentialVaultService): name=db_credential.name, credential_type=CredentialType.CREDIT_CARD, ) + elif isinstance(data, AzureCredentialVaultService._SecretCredentialDataImage): + return CredentialItem( + item_id=db_credential.item_id, + credential=SecretCredential(secret_value=data.secret_value, secret_label=data.secret_label), + name=db_credential.name, + credential_type=CredentialType.SECRET, + ) else: raise TypeError(f"Invalid credential type: {type(data)}") async def _create_azure_secret_item( self, organization_id: str, - credential: PasswordCredential | CreditCardCredential, + credential: PasswordCredential | CreditCardCredential | SecretCredential, ) -> str: if isinstance(credential, PasswordCredential): data = AzureCredentialVaultService._PasswordCredentialDataImage( @@ -153,6 +167,12 @@ class AzureCredentialVaultService(CredentialVaultService): card_brand=credential.card_brand, card_holder_name=credential.card_holder_name, ) + elif isinstance(credential, SecretCredential): + data = AzureCredentialVaultService._SecretCredentialDataImage( + type="secret", + secret_value=credential.secret_value, + secret_label=credential.secret_label, + ) else: raise TypeError(f"Invalid credential type: {type(credential)}") diff --git a/skyvern/forge/sdk/services/credential/credential_vault_service.py b/skyvern/forge/sdk/services/credential/credential_vault_service.py index 8d5ee55b..509382a6 100644 --- a/skyvern/forge/sdk/services/credential/credential_vault_service.py +++ b/skyvern/forge/sdk/services/credential/credential_vault_service.py @@ -68,5 +68,19 @@ class CredentialVaultService(ABC): card_brand=data.credential.card_brand, totp_identifier=None, ) + elif data.credential_type == CredentialType.SECRET: + return await app.DATABASE.create_credential( + organization_id=organization_id, + name=data.name, + vault_type=vault_type, + item_id=item_id, + credential_type=data.credential_type, + username=None, + totp_type="none", + card_last4=None, + card_brand=None, + totp_identifier=None, + secret_label=data.credential.secret_label, + ) else: raise Exception(f"Unsupported credential type: {data.credential_type}")