Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/app/api/docs/credentials/delete_all_by_org_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Delete all credentials for a specific organization and project.

Permanently removes all provider credentials associated with the specified organization and project IDs. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Delete credentials for a specific provider within an organization and project.

Permanently removes credentials for a specific provider from the specified organization and project. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID
- **provider**: Provider name (e.g., `openai`, `langfuse`, `google`, `sarvamai`, `elevenlabs`)
2 changes: 1 addition & 1 deletion backend/app/api/docs/credentials/get_provider.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Get credentials for a specific provider.

Retrieves decrypted credentials for a specific provider (e.g., `openai`, `langfuse`) for the current organization and project.
Retrieves credentials for a specific provider (e.g., `openai`, `langfuse`) for the current organization and project. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If credentials for the provider are not configured, `null` is returned.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Get credentials for a specific provider within an organization and project.

Retrieves credentials for a specific provider (e.g., `openai`, `langfuse`) for the specified organization and project. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If credentials for the provider are not configured, `null` is returned. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID
- **provider**: Provider name (e.g., `openai`, `langfuse`, `google`, `sarvamai`, `elevenlabs`)
2 changes: 1 addition & 1 deletion backend/app/api/docs/credentials/list.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Get all credentials for current organization and project.

Returns list of all provider credentials associated with your organization and project.
Returns a list of all provider credentials associated with your organization and project. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If no credentials are configured, an empty list is returned.
12 changes: 12 additions & 0 deletions backend/app/api/docs/credentials/list_by_org_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Get all credentials for a specific organization and project.

Retrieves all provider credentials associated with the specified organization and project IDs. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If no credentials are configured, an empty list is returned. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID

### Supported Providers:
- **LLM:** openai, sarvamai, google(gemini)
- **Observability:** langfuse
- **Audio:** elevenlabs
33 changes: 32 additions & 1 deletion backend/app/api/docs/credentials/update.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
Update credentials for a specific provider.

Updates existing provider credentials for the current organization and project. Provider and credential fields must be provided.
Updates existing provider credentials for the current organization and project. If the credentials for the specified provider don't exist yet, they will be **created** automatically (upsert behavior). The `provider` and `credential` fields are required.

The `credential` field accepts **two formats** (both work the same):

### Nested format (same as create endpoint):
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"openai": {
"api_key": "sk-proj-..."
}
}
}
```

### Flat format:
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"api_key": "sk-proj-..."
}
}
```

### Supported Providers:
- **LLM:** openai, sarvamai, google(gemini)
- **Observability:** langfuse
- **Audio:** elevenlabs
38 changes: 38 additions & 0 deletions backend/app/api/docs/credentials/update_by_org_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Update credentials for a specific provider within an organization and project.

Updates existing provider credentials for the specified organization and project. If the credentials for the specified provider don't exist yet, they will be **created** automatically (upsert behavior). Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID

The `credential` field accepts **two formats** (both work the same):

### Nested format (same as create endpoint):
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"openai": {
"api_key": "sk-proj-..."
}
}
}
```

### Flat format:
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"api_key": "sk-proj-..."
}
}
```

### Supported Providers:
- **LLM:** openai, sarvamai, google(gemini)
- **Observability:** langfuse
- **Audio:** elevenlabs
159 changes: 153 additions & 6 deletions backend/app/api/routes/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from app.api.deps import AuthContextDep, SessionDep
from app.api.permissions import Permission, require_permission
from app.core.exception_handlers import HTTPException
from app.core.providers import validate_provider
from app.core.providers import mask_credential_fields, validate_provider
from app.crud.credentials import (
get_creds_by_org,
get_provider_credential,
Expand Down Expand Up @@ -67,15 +67,13 @@ def read_credential(
org_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
if not creds:
raise HTTPException(status_code=404, detail="Credentials not found")

return APIResponse.success_response([cred.to_public() for cred in creds])


@router.get(
"/provider/{provider}",
response_model=APIResponse[dict],
response_model=APIResponse[dict | None],
description=load_description("credentials/get_provider.md"),
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
Expand All @@ -97,9 +95,11 @@ def read_provider_credential(
project_id=_current_user.project_.id,
)
if credential is None:
raise HTTPException(status_code=404, detail="Provider credentials not found")
return APIResponse.success_response(None)

return APIResponse.success_response(credential)
return APIResponse.success_response(
mask_credential_fields(provider_enum, credential)
)


@router.patch(
Expand Down Expand Up @@ -184,3 +184,150 @@ def delete_all_credentials(
return APIResponse.success_response(
{"message": "All credentials deleted successfully"}
)


# --- Endpoints with explicit org_id and project_id path parameters ---


@router.get(
"/{org_id}/{project_id}",
response_model=APIResponse[list[CredsPublic]],
description=load_description("credentials/list_by_org_project.md"),
dependencies=[Depends(require_permission(Permission.SUPERUSER))],
)
def read_credentials_by_org_project(
*,
session: SessionDep,
org_id: int,
project_id: int,
_current_user: AuthContextDep,
):
creds = get_creds_by_org(
session=session,
org_id=org_id,
project_id=project_id,
)

return APIResponse.success_response([cred.to_public() for cred in creds])


@router.get(
"/{org_id}/{project_id}/provider/{provider}",
response_model=APIResponse[dict | None],
description=load_description("credentials/get_provider_by_org_project.md"),
dependencies=[Depends(require_permission(Permission.SUPERUSER))],
)
def read_provider_credential_by_org_project(
*,
session: SessionDep,
org_id: int,
project_id: int,
provider: str,
_current_user: AuthContextDep,
):
try:
provider_enum = validate_provider(provider)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

credential = get_provider_credential(
session=session,
org_id=org_id,
provider=provider_enum,
project_id=project_id,
)
if credential is None:
return APIResponse.success_response(None)

return APIResponse.success_response(
mask_credential_fields(provider_enum, credential)
)


@router.patch(
"/{org_id}/{project_id}",
response_model=APIResponse[list[CredsPublic]],
description=load_description("credentials/update_by_org_project.md"),
dependencies=[Depends(require_permission(Permission.SUPERUSER))],
)
def update_credential_by_org_project(
*,
session: SessionDep,
org_id: int,
project_id: int,
creds_in: CredsUpdate,
_current_user: AuthContextDep,
):
if not creds_in or not creds_in.provider or not creds_in.credential:
logger.error(
f"[update_credential_by_org_project] Invalid input | organization_id: {org_id}, project_id: {project_id}"
)
raise HTTPException(
status_code=400, detail="Provider and credential must be provided"
)

updated_credential = update_creds_for_org(
session=session,
org_id=org_id,
creds_in=creds_in,
project_id=project_id,
)

return APIResponse.success_response(
[cred.to_public() for cred in updated_credential]
)


@router.delete(
"/{org_id}/{project_id}/provider/{provider}",
response_model=APIResponse[dict],
description=load_description("credentials/delete_provider_by_org_project.md"),
dependencies=[Depends(require_permission(Permission.SUPERUSER))],
)
def delete_provider_credential_by_org_project(
*,
session: SessionDep,
org_id: int,
project_id: int,
provider: str,
_current_user: AuthContextDep,
):
try:
provider_enum = validate_provider(provider)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

remove_provider_credential(
session=session,
org_id=org_id,
provider=provider_enum,
project_id=project_id,
)

return APIResponse.success_response(
{"message": "Provider credentials removed successfully"}
)


@router.delete(
"/{org_id}/{project_id}",
response_model=APIResponse[dict],
description=load_description("credentials/delete_all_by_org_project.md"),
dependencies=[Depends(require_permission(Permission.SUPERUSER))],
)
def delete_all_credentials_by_org_project(
*,
session: SessionDep,
org_id: int,
project_id: int,
_current_user: AuthContextDep,
):
remove_creds_for_org(
session=session,
org_id=org_id,
project_id=project_id,
)

return APIResponse.success_response(
{"message": "All credentials deleted successfully"}
)
50 changes: 43 additions & 7 deletions backend/app/core/providers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from typing import Dict, List
from typing import Any, Dict, List
from enum import Enum
from dataclasses import dataclass
from dataclasses import dataclass, field

logger = logging.getLogger(__name__)

Expand All @@ -21,17 +21,27 @@ class ProviderConfig:
"""Configuration for a provider including its required credential fields."""

required_fields: List[str]
sensitive_fields: List[str] = field(default_factory=list)


# Provider configurations
PROVIDER_CONFIGS: Dict[Provider, ProviderConfig] = {
Provider.OPENAI: ProviderConfig(required_fields=["api_key"]),
Provider.OPENAI: ProviderConfig(
required_fields=["api_key"], sensitive_fields=["api_key"]
),
Provider.LANGFUSE: ProviderConfig(
required_fields=["secret_key", "public_key", "host"]
required_fields=["secret_key", "public_key", "host"],
sensitive_fields=["secret_key"],
),
Provider.GOOGLE: ProviderConfig(
required_fields=["api_key"], sensitive_fields=["api_key"]
),
Provider.SARVAMAI: ProviderConfig(
required_fields=["api_key"], sensitive_fields=["api_key"]
),
Provider.ELEVENLABS: ProviderConfig(
required_fields=["api_key"], sensitive_fields=["api_key"]
),
Provider.GOOGLE: ProviderConfig(required_fields=["api_key"]),
Provider.SARVAMAI: ProviderConfig(required_fields=["api_key"]),
Provider.ELEVENLABS: ProviderConfig(required_fields=["api_key"]),
}


Expand Down Expand Up @@ -86,3 +96,29 @@ def validate_provider_credentials(provider: str, credentials: Dict[str, str]) ->
def get_supported_providers() -> List[str]:
"""Return a list of all supported provider names."""
return [p.value for p in Provider]


def mask_credential_fields(
provider: str, credentials: Dict[str, Any]
) -> Dict[str, Any]:
"""Mask sensitive fields in a credential dict for the given provider.

Non-sensitive fields (e.g., langfuse `public_key`, `host`) are returned as-is.
Unknown providers are returned with no masking.
"""
from app.utils import mask_string

if not credentials:
return credentials

try:
provider_enum = Provider(provider.lower())
except ValueError:
return credentials

sensitive_fields = PROVIDER_CONFIGS[provider_enum].sensitive_fields
masked = dict(credentials)
for field_name in sensitive_fields:
if field_name in masked and isinstance(masked[field_name], str):
masked[field_name] = mask_string(masked[field_name])
return masked
Loading