[Backend] Add SECRET credential type for storing generic sensitive values (#4246)
This commit is contained in:
@@ -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 ###
|
||||||
@@ -4573,6 +4573,7 @@ class AgentDB:
|
|||||||
card_last4: str | None,
|
card_last4: str | None,
|
||||||
card_brand: str | None,
|
card_brand: str | None,
|
||||||
totp_identifier: str | None = None,
|
totp_identifier: str | None = None,
|
||||||
|
secret_label: str | None = None,
|
||||||
) -> Credential:
|
) -> Credential:
|
||||||
async with self.Session() as session:
|
async with self.Session() as session:
|
||||||
credential = CredentialModel(
|
credential = CredentialModel(
|
||||||
@@ -4586,6 +4587,7 @@ class AgentDB:
|
|||||||
totp_identifier=totp_identifier,
|
totp_identifier=totp_identifier,
|
||||||
card_last4=card_last4,
|
card_last4=card_last4,
|
||||||
card_brand=card_brand,
|
card_brand=card_brand,
|
||||||
|
secret_label=secret_label,
|
||||||
)
|
)
|
||||||
session.add(credential)
|
session.add(credential)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|||||||
@@ -901,6 +901,7 @@ class CredentialModel(Base):
|
|||||||
totp_identifier = Column(String, nullable=True, default=None)
|
totp_identifier = Column(String, nullable=True, default=None)
|
||||||
card_last4 = Column(String, nullable=True)
|
card_last4 = Column(String, nullable=True)
|
||||||
card_brand = 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)
|
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from skyvern.forge.sdk.schemas.credentials import (
|
|||||||
CredentialVaultType,
|
CredentialVaultType,
|
||||||
CreditCardCredentialResponse,
|
CreditCardCredentialResponse,
|
||||||
PasswordCredentialResponse,
|
PasswordCredentialResponse,
|
||||||
|
SecretCredentialResponse,
|
||||||
)
|
)
|
||||||
from skyvern.forge.sdk.schemas.organizations import (
|
from skyvern.forge.sdk.schemas.organizations import (
|
||||||
AzureClientSecretCredentialResponse,
|
AzureClientSecretCredentialResponse,
|
||||||
@@ -267,6 +268,14 @@ async def create_credential(
|
|||||||
credential_type=data.credential_type,
|
credential_type=data.credential_type,
|
||||||
name=data.name,
|
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:
|
else:
|
||||||
raise HTTPException(status_code=400, detail=f"Unsupported credential type: {data.credential_type}")
|
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,
|
credential_type=credential.credential_type,
|
||||||
name=credential.name,
|
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:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="Credential type not supported")
|
raise HTTPException(status_code=400, detail="Credential type not supported")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class CredentialType(StrEnum):
|
|||||||
|
|
||||||
PASSWORD = "password"
|
PASSWORD = "password"
|
||||||
CREDIT_CARD = "credit_card"
|
CREDIT_CARD = "credit_card"
|
||||||
|
SECRET = "secret"
|
||||||
|
|
||||||
|
|
||||||
class TotpType(StrEnum):
|
class TotpType(StrEnum):
|
||||||
@@ -49,6 +50,12 @@ class CreditCardCredentialResponse(BaseModel):
|
|||||||
brand: str = Field(..., description="Brand of the credit card", examples=["visa"])
|
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):
|
class PasswordCredential(BaseModel):
|
||||||
"""Base model for password credentials."""
|
"""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):
|
class CredentialItem(BaseModel):
|
||||||
"""Model representing a credential item in the system."""
|
"""Model representing a credential item in the system."""
|
||||||
|
|
||||||
item_id: str = Field(..., description="Unique identifier for the credential item", examples=["cred_1234567890"])
|
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"])
|
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_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):
|
class CreateCredentialRequest(BaseModel):
|
||||||
@@ -129,7 +145,7 @@ class CreateCredentialRequest(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(..., description="Name of the credential", examples=["Amazon Login"])
|
name: str = Field(..., description="Name of the credential", examples=["Amazon Login"])
|
||||||
credential_type: CredentialType = Field(..., description="Type of credential to create")
|
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",
|
description="The credential data to store",
|
||||||
examples=[{"username": "user@example.com", "password": "securepassword123"}],
|
examples=[{"username": "user@example.com", "password": "securepassword123"}],
|
||||||
@@ -140,7 +156,7 @@ class CredentialResponse(BaseModel):
|
|||||||
"""Response model for credential operations."""
|
"""Response model for credential operations."""
|
||||||
|
|
||||||
credential_id: str = Field(..., description="Unique identifier for the credential", examples=["cred_1234567890"])
|
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"
|
..., description="The credential data"
|
||||||
)
|
)
|
||||||
credential_type: CredentialType = Field(..., description="Type of the credential")
|
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_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")
|
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")
|
created_at: datetime = Field(..., description="Timestamp when the credential was created")
|
||||||
modified_at: datetime = Field(..., description="Timestamp when the credential was last modified")
|
modified_at: datetime = Field(..., description="Timestamp when the credential was last modified")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from skyvern.forge.sdk.schemas.credentials import (
|
|||||||
CredentialVaultType,
|
CredentialVaultType,
|
||||||
CreditCardCredential,
|
CreditCardCredential,
|
||||||
PasswordCredential,
|
PasswordCredential,
|
||||||
|
SecretCredential,
|
||||||
)
|
)
|
||||||
from skyvern.forge.sdk.services.credential.credential_vault_service import CredentialVaultService
|
from skyvern.forge.sdk.services.credential.credential_vault_service import CredentialVaultService
|
||||||
|
|
||||||
@@ -36,8 +37,14 @@ class AzureCredentialVaultService(CredentialVaultService):
|
|||||||
card_brand: str
|
card_brand: str
|
||||||
card_holder_name: str
|
card_holder_name: str
|
||||||
|
|
||||||
|
class _SecretCredentialDataImage(BaseModel):
|
||||||
|
type: Literal["secret"]
|
||||||
|
secret_value: str
|
||||||
|
secret_label: str | None = None
|
||||||
|
|
||||||
_CredentialDataImage = Annotated[
|
_CredentialDataImage = Annotated[
|
||||||
Union[_PasswordCredentialDataImage, _CreditCardCredentialDataImage], Field(discriminator="type")
|
Union[_PasswordCredentialDataImage, _CreditCardCredentialDataImage, _SecretCredentialDataImage],
|
||||||
|
Field(discriminator="type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, client: AsyncAzureVaultClient, vault_name: str):
|
def __init__(self, client: AsyncAzureVaultClient, vault_name: str):
|
||||||
@@ -128,13 +135,20 @@ class AzureCredentialVaultService(CredentialVaultService):
|
|||||||
name=db_credential.name,
|
name=db_credential.name,
|
||||||
credential_type=CredentialType.CREDIT_CARD,
|
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:
|
else:
|
||||||
raise TypeError(f"Invalid credential type: {type(data)}")
|
raise TypeError(f"Invalid credential type: {type(data)}")
|
||||||
|
|
||||||
async def _create_azure_secret_item(
|
async def _create_azure_secret_item(
|
||||||
self,
|
self,
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
credential: PasswordCredential | CreditCardCredential,
|
credential: PasswordCredential | CreditCardCredential | SecretCredential,
|
||||||
) -> str:
|
) -> str:
|
||||||
if isinstance(credential, PasswordCredential):
|
if isinstance(credential, PasswordCredential):
|
||||||
data = AzureCredentialVaultService._PasswordCredentialDataImage(
|
data = AzureCredentialVaultService._PasswordCredentialDataImage(
|
||||||
@@ -153,6 +167,12 @@ class AzureCredentialVaultService(CredentialVaultService):
|
|||||||
card_brand=credential.card_brand,
|
card_brand=credential.card_brand,
|
||||||
card_holder_name=credential.card_holder_name,
|
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:
|
else:
|
||||||
raise TypeError(f"Invalid credential type: {type(credential)}")
|
raise TypeError(f"Invalid credential type: {type(credential)}")
|
||||||
|
|
||||||
|
|||||||
@@ -68,5 +68,19 @@ class CredentialVaultService(ABC):
|
|||||||
card_brand=data.credential.card_brand,
|
card_brand=data.credential.card_brand,
|
||||||
totp_identifier=None,
|
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:
|
else:
|
||||||
raise Exception(f"Unsupported credential type: {data.credential_type}")
|
raise Exception(f"Unsupported credential type: {data.credential_type}")
|
||||||
|
|||||||
Reference in New Issue
Block a user