Skip to content

Commit d3297ac

Browse files
authored
Credentials: Fetching ID from API key (#354)
* fetch org_id and project_id from API key * taking org_id and project_id from apikey * few more cleanups for credential CRUD * few more cleanups for credential CRUD * fixing typo * coderabbit suggestions * cleanup unnecessary variables * reverting naming variables * nitty gritty cleanups * updating testcases with updatederror messages
1 parent 721d120 commit d3297ac

File tree

6 files changed

+399
-320
lines changed

6 files changed

+399
-320
lines changed
Lines changed: 95 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
from typing import List
2-
31
from fastapi import APIRouter, Depends
42

5-
from app.api.deps import SessionDep, get_current_active_superuser
3+
from app.api.deps import SessionDep, get_current_user_org_project
64
from app.crud.credentials import (
75
get_creds_by_org,
86
get_provider_credential,
@@ -11,10 +9,7 @@
119
update_creds_for_org,
1210
remove_provider_credential,
1311
)
14-
from app.crud import validate_organization, validate_project
15-
from app.models import CredsCreate, CredsPublic, CredsUpdate
16-
from app.models.organization import Organization
17-
from app.models.project import Project
12+
from app.models import CredsCreate, CredsPublic, CredsUpdate, UserProjectOrg
1813
from app.utils import APIResponse
1914
from app.core.providers import validate_provider
2015
from app.core.exception_handlers import HTTPException
@@ -24,31 +19,25 @@
2419

2520
@router.post(
2621
"/",
27-
dependencies=[Depends(get_current_active_superuser)],
28-
response_model=APIResponse[List[CredsPublic]],
29-
summary="Create new credentials for an organization and project",
30-
description="Creates new credentials for a specific organization and project combination. This endpoint requires superuser privileges. Each organization can have different credentials for different providers and projects. Only one credential per provider is allowed per organization-project combination.",
22+
response_model=APIResponse[list[CredsPublic]],
23+
summary="Create new credentials for the current organization and project",
24+
description="Creates new credentials for the caller's organization and project. Each organization can have different credentials for different providers and projects. Only one credential per provider is allowed per organization-project combination.",
3125
)
32-
def create_new_credential(*, session: SessionDep, creds_in: CredsCreate):
33-
# Validate organization
34-
validate_organization(session, creds_in.organization_id)
35-
36-
# Validate project if provided
37-
if creds_in.project_id:
38-
project = validate_project(session, creds_in.project_id)
39-
if project.organization_id != creds_in.organization_id:
40-
raise HTTPException(
41-
status_code=400,
42-
detail="Project does not belong to the specified organization",
43-
)
26+
def create_new_credential(
27+
*,
28+
session: SessionDep,
29+
creds_in: CredsCreate,
30+
_current_user: UserProjectOrg = Depends(get_current_user_org_project),
31+
):
32+
# Project comes from API key context; no cross-org check needed here
4433

4534
# Prevent duplicate credentials
4635
for provider in creds_in.credential.keys():
4736
existing_cred = get_provider_credential(
4837
session=session,
49-
org_id=creds_in.organization_id,
38+
org_id=_current_user.organization_id,
5039
provider=provider,
51-
project_id=creds_in.project_id,
40+
project_id=_current_user.project_id,
5241
)
5342
if existing_cred:
5443
raise HTTPException(
@@ -60,95 +49,121 @@ def create_new_credential(*, session: SessionDep, creds_in: CredsCreate):
6049
)
6150

6251
# Create credentials
63-
new_creds = set_creds_for_org(session=session, creds_add=creds_in)
64-
if not new_creds:
52+
created_creds = set_creds_for_org(
53+
session=session,
54+
creds_add=creds_in,
55+
organization_id=_current_user.organization_id,
56+
project_id=_current_user.project_id,
57+
)
58+
if not created_creds:
6559
raise Exception(status_code=500, detail="Failed to create credentials")
6660

67-
return APIResponse.success_response([cred.to_public() for cred in new_creds])
61+
return APIResponse.success_response([cred.to_public() for cred in created_creds])
6862

6963

7064
@router.get(
71-
"/{org_id}",
72-
dependencies=[Depends(get_current_active_superuser)],
73-
response_model=APIResponse[List[CredsPublic]],
74-
summary="Get all credentials for an organization and project",
75-
description="Retrieves all provider credentials associated with a specific organization and project combination. If project_id is not provided, returns credentials for the organization level. This endpoint requires superuser privileges.",
65+
"/",
66+
response_model=APIResponse[list[CredsPublic]],
67+
summary="Get all credentials for current org and project",
68+
description="Retrieves all provider credentials associated with the caller's organization and project.",
7669
)
77-
def read_credential(*, session: SessionDep, org_id: int, project_id: int | None = None):
78-
creds = get_creds_by_org(session=session, org_id=org_id, project_id=project_id)
70+
def read_credential(
71+
*,
72+
session: SessionDep,
73+
_current_user: UserProjectOrg = Depends(get_current_user_org_project),
74+
):
75+
creds = get_creds_by_org(
76+
session=session,
77+
org_id=_current_user.organization_id,
78+
project_id=_current_user.project_id,
79+
)
7980
if not creds:
8081
raise HTTPException(status_code=404, detail="Credentials not found")
8182

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

8485

8586
@router.get(
86-
"/{org_id}/{provider}",
87-
dependencies=[Depends(get_current_active_superuser)],
87+
"/provider/{provider}",
8888
response_model=APIResponse[dict],
89-
summary="Get specific provider credentials for an organization and project",
90-
description="Retrieves credentials for a specific provider (e.g., 'openai', 'anthropic') for a given organization and project combination. If project_id is not provided, returns organization-level credentials. This endpoint requires superuser privileges.",
89+
summary="Get specific provider credentials for current org and project",
90+
description="Retrieves credentials for a specific provider (e.g., 'openai', 'anthropic') for the caller's organization and project.",
9191
)
9292
def read_provider_credential(
93-
*, session: SessionDep, org_id: int, provider: str, project_id: int | None = None
93+
*,
94+
session: SessionDep,
95+
provider: str,
96+
_current_user: UserProjectOrg = Depends(get_current_user_org_project),
9497
):
9598
provider_enum = validate_provider(provider)
96-
provider_creds = get_provider_credential(
99+
credential = get_provider_credential(
97100
session=session,
98-
org_id=org_id,
101+
org_id=_current_user.organization_id,
99102
provider=provider_enum,
100-
project_id=project_id,
103+
project_id=_current_user.project_id,
101104
)
102-
if provider_creds is None:
105+
if credential is None:
103106
raise HTTPException(status_code=404, detail="Provider credentials not found")
104107

105-
return APIResponse.success_response(provider_creds)
108+
return APIResponse.success_response(credential)
106109

107110

108111
@router.patch(
109-
"/{org_id}",
110-
dependencies=[Depends(get_current_active_superuser)],
111-
response_model=APIResponse[List[CredsPublic]],
112-
summary="Update organization and project credentials",
113-
description="Updates credentials for a specific organization and project combination. Can update specific provider credentials or add new providers. If project_id is provided in the update, credentials will be moved to that project. This endpoint requires superuser privileges.",
112+
"/",
113+
response_model=APIResponse[list[CredsPublic]],
114+
summary="Update credentials for current org and project",
115+
description="Updates credentials for a specific provider of the caller's organization and project.",
114116
)
115-
def update_credential(*, session: SessionDep, org_id: int, creds_in: CredsUpdate):
116-
validate_organization(session, org_id)
117+
def update_credential(
118+
*,
119+
session: SessionDep,
120+
creds_in: CredsUpdate,
121+
_current_user: UserProjectOrg = Depends(get_current_user_org_project),
122+
):
117123
if not creds_in or not creds_in.provider or not creds_in.credential:
118124
raise HTTPException(
119125
status_code=400, detail="Provider and credential must be provided"
120126
)
121127

122-
updated_creds = update_creds_for_org(
123-
session=session, org_id=org_id, creds_in=creds_in
128+
# Pass project_id directly to the CRUD function since CredsUpdate no longer has this field
129+
updated_credential = update_creds_for_org(
130+
session=session,
131+
org_id=_current_user.organization_id,
132+
creds_in=creds_in,
133+
project_id=_current_user.project_id,
124134
)
125135

126-
return APIResponse.success_response([cred.to_public() for cred in updated_creds])
136+
return APIResponse.success_response(
137+
[cred.to_public() for cred in updated_credential]
138+
)
127139

128140

129141
@router.delete(
130-
"/{org_id}/{provider}",
131-
dependencies=[Depends(get_current_active_superuser)],
142+
"/provider/{provider}",
132143
response_model=APIResponse[dict],
133-
summary="Delete specific provider credentials for an organization and project",
144+
summary="Delete specific provider credentials for current org and project",
134145
)
135146
def delete_provider_credential(
136-
*, session: SessionDep, org_id: int, provider: str, project_id: int | None = None
147+
*,
148+
session: SessionDep,
149+
provider: str,
150+
_current_user: UserProjectOrg = Depends(get_current_user_org_project),
137151
):
138152
provider_enum = validate_provider(provider)
139-
if not provider_enum:
140-
raise HTTPException(status_code=400, detail="Invalid provider")
141153
provider_creds = get_provider_credential(
142154
session=session,
143-
org_id=org_id,
155+
org_id=_current_user.organization_id,
144156
provider=provider_enum,
145-
project_id=project_id,
157+
project_id=_current_user.project_id,
146158
)
147159
if provider_creds is None:
148160
raise HTTPException(status_code=404, detail="Provider credentials not found")
149161

150-
updated_creds = remove_provider_credential(
151-
session=session, org_id=org_id, provider=provider_enum, project_id=project_id
162+
remove_provider_credential(
163+
session=session,
164+
org_id=_current_user.organization_id,
165+
provider=provider_enum,
166+
project_id=_current_user.project_id,
152167
)
153168

154169
return APIResponse.success_response(
@@ -157,19 +172,26 @@ def delete_provider_credential(
157172

158173

159174
@router.delete(
160-
"/{org_id}",
161-
dependencies=[Depends(get_current_active_superuser)],
175+
"/",
162176
response_model=APIResponse[dict],
163-
summary="Delete all credentials for an organization and project",
164-
description="Removes all credentials for a specific organization and project combination. If project_id is provided, only removes credentials for that project. This is a soft delete operation that marks credentials as inactive. This endpoint requires superuser privileges.",
177+
summary="Delete all credentials for current org and project",
178+
description="Removes all credentials for the caller's organization and project. This is a soft delete operation that marks credentials as inactive.",
165179
)
166180
def delete_all_credentials(
167-
*, session: SessionDep, org_id: int, project_id: int | None = None
181+
*,
182+
session: SessionDep,
183+
_current_user: UserProjectOrg = Depends(get_current_user_org_project),
168184
):
169-
creds = remove_creds_for_org(session=session, org_id=org_id, project_id=project_id)
185+
creds = remove_creds_for_org(
186+
session=session,
187+
org_id=_current_user.organization_id,
188+
project_id=_current_user.project_id,
189+
)
170190
if not creds:
171191
raise HTTPException(
172-
status_code=404, detail="Credentials for organization not found"
192+
status_code=404, detail="Credentials for organization/project not found"
173193
)
174194

175-
return APIResponse.success_response({"message": "Credentials deleted successfully"})
195+
return APIResponse.success_response(
196+
{"message": "All credentials deleted successfully"}
197+
)

backend/app/crud/credentials.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from app.core.exception_handlers import HTTPException
1515

1616

17-
def set_creds_for_org(*, session: Session, creds_add: CredsCreate) -> List[Credential]:
17+
def set_creds_for_org(
18+
*, session: Session, creds_add: CredsCreate, organization_id: int, project_id: int
19+
) -> List[Credential]:
1820
"""Set credentials for an organization. Creates a separate row for each provider."""
1921
created_credentials = []
2022

@@ -31,8 +33,8 @@ def set_creds_for_org(*, session: Session, creds_add: CredsCreate) -> List[Crede
3133

3234
# Create a row for each provider
3335
credential = Credential(
34-
organization_id=creds_add.organization_id,
35-
project_id=creds_add.project_id,
36+
organization_id=organization_id,
37+
project_id=project_id,
3638
is_active=creds_add.is_active,
3739
provider=provider,
3840
credential=encrypted_credentials,
@@ -127,7 +129,11 @@ def get_providers(
127129

128130

129131
def update_creds_for_org(
130-
*, session: Session, org_id: int, creds_in: CredsUpdate
132+
*,
133+
session: Session,
134+
org_id: int,
135+
creds_in: CredsUpdate,
136+
project_id: Optional[int] = None,
131137
) -> List[Credential]:
132138
"""Updates credentials for a specific provider of an organization."""
133139
if not creds_in.provider or not creds_in.credential:
@@ -143,9 +149,7 @@ def update_creds_for_org(
143149
Credential.organization_id == org_id,
144150
Credential.provider == creds_in.provider,
145151
Credential.is_active == True,
146-
Credential.project_id == creds_in.project_id
147-
if creds_in.project_id is not None
148-
else True,
152+
Credential.project_id == project_id if project_id is not None else True,
149153
)
150154
creds = session.exec(statement).first()
151155
if creds is None:

backend/app/models/credentials.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ class CredsBase(SQLModel):
1616
is_active: bool = True
1717

1818

19-
class CredsCreate(CredsBase):
19+
class CredsCreate(SQLModel):
2020
"""Create new credentials for an organization.
2121
The credential field should be a dictionary mapping provider names to their credentials.
2222
Example: {"openai": {"api_key": "..."}, "langfuse": {"public_key": "..."}}
2323
"""
2424

25+
is_active: bool = True
2526
credential: Dict[str, Any] = Field(
2627
default=None,
2728
description="Dictionary mapping provider names to their credentials",
@@ -42,9 +43,6 @@ class CredsUpdate(SQLModel):
4243
is_active: Optional[bool] = Field(
4344
default=None, description="Whether the credentials are active"
4445
)
45-
project_id: Optional[int] = Field(
46-
default=None, description="Project ID to associate with these credentials"
47-
)
4846

4947

5048
class Credential(CredsBase, table=True):

0 commit comments

Comments
 (0)