Skip to content

Commit 869f688

Browse files
committed
feat(admmin): add superadmin creation at startup
- remove MasterNotAllowedException exception - remove hardcoded admin bypass - delete MASTER_USER_ID and MASTER_KEY_ID for MASTER_ID - add auth_master_username setting - add auth_master_password setting - remove use of master key as API key - replace 0 refs to MASTER_ID global variable - add setup_master method in lifespan (WIP: prints still present) - rename master_key to secret_key in IdentityAccessManager
1 parent 4c4ddb6 commit 869f688

19 files changed

Lines changed: 219 additions & 257 deletions

api/domain/key/entities.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
from api.helpers._identityaccessmanager import IdentityAccessManager
55
from api.utils.exceptions import InvalidAPIKeyException
66

7-
MASTER_USER_ID = 0
8-
MASTER_KEY_ID = 0
7+
MASTER_ID: int = 0
98

109

1110
class KeyClaims(BaseModel):
@@ -19,10 +18,6 @@ class Key(BaseModel):
1918
value: str = Field(..., description="The raw API key value")
2019

2120
def decode(self, secret_key: str) -> KeyClaims:
22-
# TODO: Remove this hardcoded master key logic
23-
if self.value == secret_key:
24-
return KeyClaims(user_id=MASTER_USER_ID, key_id=MASTER_KEY_ID)
25-
2621
if not self.value.startswith(IdentityAccessManager.TOKEN_PREFIX):
2722
raise InvalidAPIKeyException()
2823

api/domain/userinfo/entities.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pydantic import BaseModel, Field
22

3-
from api.domain.key.entities import MASTER_USER_ID
3+
from api.domain.key.entities import MASTER_ID
44
from api.domain.role.entities import Limit, PermissionType
55

66

@@ -20,4 +20,4 @@ class UserInfo(BaseModel):
2020
@property
2121
def is_admin(self) -> bool:
2222
# TODO: remove self.id == 0 when master user is refacto
23-
return PermissionType.ADMIN in self.permissions or self.id == MASTER_USER_ID
23+
return PermissionType.ADMIN in self.permissions or self.id == MASTER_ID

api/helpers/_accesscontroller.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -83,35 +83,17 @@ async def _check_api_key(request: Request, api_key: HTTPAuthorizationCredentials
8383
if not api_key.credentials:
8484
raise InvalidAPIKeyException()
8585

86-
# master user can do anything
87-
if api_key.credentials == global_context.identity_access_manager.secret_key:
88-
user_info = UserInfo(
89-
id=0,
90-
email="master",
91-
name="master",
92-
budget=None,
93-
limits=[],
94-
permissions=[permission for permission in PermissionType],
95-
expires=None,
96-
created=0,
97-
updated=0,
98-
organization_id=0,
99-
priority=0,
100-
)
101-
key_id = 0
102-
key_name = "master"
103-
else:
104-
user_id, key_id, key_name = await global_context.identity_access_manager.check_token(
105-
postgres_session=postgres_session, token=api_key.credentials
106-
)
107-
if not user_id:
108-
raise InvalidAPIKeyException()
86+
user_id, key_id, key_name = await global_context.identity_access_manager.check_token(
87+
postgres_session=postgres_session, token=api_key.credentials
88+
)
89+
if not user_id:
90+
raise InvalidAPIKeyException()
10991

110-
user_info = await global_context.identity_access_manager.get_user_info(postgres_session=postgres_session, user_id=user_id)
92+
user_info = await global_context.identity_access_manager.get_user_info(postgres_session=postgres_session, user_id=user_id)
11193

112-
# invalid token if user is expired, except for /me and /me/role endpoints
113-
if user_info.expires and user_info.expires < time.time() and not request.url.path.endswith(EndpointRoute.ME_INFO):
114-
raise InvalidAPIKeyException()
94+
# invalid token if user is expired, except for /me and /me/role endpoints
95+
if user_info.expires and user_info.expires < time.time() and not request.url.path.endswith(EndpointRoute.ME_INFO):
96+
raise InvalidAPIKeyException()
11597

11698
return user_info, key_id, key_name
11799

api/helpers/_documentmanager.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
ChunkingFailedException,
3030
CollectionNotFoundException,
3131
DocumentNotFoundException,
32-
MasterNotAllowedException,
3332
ParsingDocumentFailedException,
3433
VectorizationFailedException,
3534
)
@@ -49,9 +48,6 @@ def __init__(self, vector_store_model: str | None, parser_manager: ParserManager
4948

5049
@staticmethod
5150
async def create_collection(postgres_session: AsyncSession, user_id: int, name: str, visibility: CollectionVisibility, description: str | None = None) -> int: # fmt: off
52-
if user_id == 0:
53-
raise MasterNotAllowedException(detail="Master user is not allowed to create collection.")
54-
5551
query = (
5652
insert(table=CollectionTable)
5753
.values(name=name, user_id=user_id, visibility=visibility, description=description)

api/helpers/_identityaccessmanager.py

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from sqlalchemy.sql import func
1111

1212
from api.schemas.admin.organizations import Organization
13-
from api.schemas.admin.roles import Limit, LimitType, PermissionType, Role
13+
from api.schemas.admin.roles import Limit, PermissionType, Role
1414
from api.schemas.admin.tokens import Token
1515
from api.schemas.admin.users import User
1616
from api.schemas.me.info import UserInfo
@@ -21,7 +21,6 @@
2121
from api.sql.models import Token as TokenTable
2222
from api.sql.models import User as UserTable
2323
from api.utils.configuration import configuration
24-
from api.utils.context import global_context
2524
from api.utils.exceptions import (
2625
DeleteOrganizationWithUsersException,
2726
DeleteRoleWithUsersException,
@@ -251,8 +250,9 @@ async def create_user(
251250
budget: float | None = None,
252251
expires: int | None = None,
253252
priority: int = 0,
253+
check_master_email: bool = True,
254254
) -> int:
255-
if email == "master":
255+
if check_master_email and email == configuration.settings.auth_master_username:
256256
raise ReservedEmailException()
257257

258258
expires = func.to_timestamp(expires) if expires is not None else None
@@ -355,7 +355,7 @@ async def update_user(
355355
# update the user
356356
email = email if email is not None else user.email
357357

358-
if email == "master":
358+
if email == configuration.settings.auth_master_username and email != user.email:
359359
raise ReservedEmailException()
360360

361361
name = name if name is not None else user.name
@@ -732,44 +732,28 @@ async def get_user(
732732
async def get_user_info(self, postgres_session: AsyncSession, user_id: int | None = None, email: str | None = None) -> UserInfo:
733733
assert user_id is not None or email is not None, "user_id or email is required"
734734

735-
if user_id == 0: # master user
736-
routers = await global_context.model_registry.get_routers(router_id=None, name=None, postgres_session=postgres_session)
737-
user = UserInfo(
738-
id=0,
739-
email="master",
740-
name="master",
741-
organization=0,
742-
budget=None,
743-
permissions=[permission for permission in PermissionType],
744-
limits=[Limit(router=router.id, type=type, value=None) for router in routers for type in LimitType],
745-
expires=None,
746-
created=0,
747-
updated=0,
748-
priority=0,
749-
)
750-
else:
751-
users = await self.get_users(postgres_session=postgres_session, user_id=user_id, email=email)
752-
user = users[0]
753-
754-
roles = await self.get_roles(postgres_session, role_id=user.role)
755-
role = roles[0]
756-
757-
# user cannot see limits on models that are not accessible by the role
758-
limits = [limit for limit in role.limits if limit.value is None or limit.value > 0]
759-
760-
user = UserInfo(
761-
id=user.id,
762-
email=user.email,
763-
name=user.name,
764-
organization=user.organization,
765-
budget=user.budget,
766-
permissions=role.permissions,
767-
limits=limits,
768-
expires=user.expires,
769-
created=user.created,
770-
updated=user.updated,
771-
priority=user.priority,
772-
)
735+
users = await self.get_users(postgres_session=postgres_session, user_id=user_id, email=email)
736+
user = users[0]
737+
738+
roles = await self.get_roles(postgres_session, role_id=user.role)
739+
role = roles[0]
740+
741+
# user cannot see limits on models that are not accessible by the role
742+
limits = [limit for limit in role.limits if limit.value is None or limit.value > 0]
743+
744+
user = UserInfo(
745+
id=user.id,
746+
email=user.email,
747+
name=user.name,
748+
organization=user.organization,
749+
budget=user.budget,
750+
permissions=role.permissions,
751+
limits=limits,
752+
expires=user.expires,
753+
created=user.created,
754+
updated=user.updated,
755+
priority=user.priority,
756+
)
773757

774758
return user
775759

@@ -787,14 +771,19 @@ async def login(self, postgres_session: AsyncSession, email: str, password: str)
787771
Tuple containing the token ID and the token of the refreshed playground token.
788772
"""
789773

790-
# TODO: Remove this authentication backdoor for the master user.
791-
if email == "master" and password == self.secret_key:
792-
return 0, self.secret_key
774+
print("======= LOGIN ATTEMPT =======")
775+
print(f"Email: {email}")
776+
print(f"Password: {password}")
793777

794778
user = await self.get_user_info(postgres_session=postgres_session, email=email) # raise UserNotFoundException (404) if user not found
795779
result = await postgres_session.execute(statement=select(UserTable.password).where(UserTable.id == user.id))
796780
hashed_password = result.scalar_one()
797781

782+
print("-- USER FOUND --")
783+
print(f"User: {user}")
784+
print(f"Result: {result}")
785+
print(f"Hashed password: {hashed_password}")
786+
798787
if not hashed_password:
799788
raise PasswordNotFoundException()
800789

@@ -803,4 +792,7 @@ async def login(self, postgres_session: AsyncSession, email: str, password: str)
803792

804793
token_id, token = await self.refresh_token(postgres_session, user_id=user.id, name=self.PLAYGROUND_KEY_NAME)
805794

795+
print("-- LOGIN SUCCESSFUL --")
796+
print(f"Token ID: {token_id}")
797+
print(f"Token: {token}")
806798
return token_id, token

api/helpers/models/_modelregistry.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sqlalchemy.ext.asyncio import AsyncSession
99

1010
from api.clients.model import BaseModelProvider as ModelProvider
11+
from api.domain.key.entities import MASTER_ID
1112
from api.infrastructure.fastapi.schemas.providers import Provider, ProviderCarbonFootprintZone, ProviderType
1213
from api.schemas.admin.routers import Router, RouterLoadBalancingStrategy
1314
from api.schemas.core.configuration import Model as ModelConfiguration
@@ -116,7 +117,7 @@ async def setup(self, models: list[ModelConfiguration], postgres_session: AsyncS
116117
load_balancing_strategy=model.load_balancing_strategy,
117118
cost_prompt_tokens=model.cost_prompt_tokens,
118119
cost_completion_tokens=model.cost_completion_tokens,
119-
user_id=0, # setup as master user
120+
user_id=MASTER_ID, # setup as master user
120121
postgres_session=postgres_session,
121122
)
122123
logger.info(f"Router {model.name} are created (id: {router_id})")
@@ -137,7 +138,7 @@ async def setup(self, models: list[ModelConfiguration], postgres_session: AsyncS
137138
try:
138139
provider_id = await self.create_provider(
139140
router_id=router.id,
140-
user_id=0, # setup as master user
141+
user_id=MASTER_ID, # setup as master user
141142
type=provider.type,
142143
url=provider.url,
143144
key=provider.key,
@@ -196,7 +197,7 @@ async def create_router(
196197
"""
197198

198199
# Create the router in database
199-
user_id = None if user_id == 0 else user_id # 0 corresponds to master user ID
200+
user_id = None if user_id == MASTER_ID else user_id
200201
try:
201202
query = (
202203
insert(RouterTable)
@@ -412,7 +413,7 @@ async def get_routers(
412413

413414
routers = []
414415
for row in router_results:
415-
user_id = 0 if row["user_id"] is None else row["user_id"] # 0 corresponds to master user ID
416+
user_id = MASTER_ID if row["user_id"] is None else row["user_id"]
416417
routers.append(
417418
Router(
418419
id=row["id"],
@@ -511,7 +512,7 @@ async def create_provider(
511512

512513
# Create provider
513514
try:
514-
user_id = None if user_id == 0 else user_id # 0 corresponds to master user ID
515+
user_id = None if user_id == MASTER_ID else user_id
515516
qos_metric = qos_metric.value if qos_metric is not None else None
516517
query = (
517518
insert(ProviderTable)
@@ -630,7 +631,7 @@ async def get_providers(
630631
providers = []
631632
for row in rows:
632633
qos_metric = Metric(row["qos_metric"]) if row["qos_metric"] is not None else None
633-
user_id = 0 if row["user_id"] is None else row["user_id"] # 0 corresponds to master user ID
634+
user_id = MASTER_ID if row["user_id"] is None else row["user_id"]
634635
providers.append(
635636
Provider(
636637
id=row["id"],

api/infrastructure/postgres/_postgresrouterrepository.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from sqlalchemy.exc import IntegrityError
33
from sqlalchemy.ext.asyncio import AsyncSession
44

5-
from api.domain.key.entities import MASTER_USER_ID
5+
from api.domain.key.entities import MASTER_ID
66
from api.domain.model import ModelType as RouterType
77
from api.domain.router import RouterRepository
88
from api.domain.router.entities import Router, RouterLoadBalancingStrategy, RouterPage, RouterSortField, SortOrder
@@ -39,7 +39,7 @@ def _select_routers_with_provider_stats() -> Select:
3939

4040
@staticmethod
4141
def _row_to_router_with_aliases(row, aliases: list[str]) -> Router:
42-
user_id = MASTER_USER_ID if row.user_id is None else row.user_id
42+
user_id = MASTER_ID if row.user_id is None else row.user_id
4343
return Router(
4444
id=row.id,
4545
name=row.name,
@@ -129,7 +129,7 @@ async def create_router(
129129
user_id: int,
130130
aliases: list[str] | None = None,
131131
) -> Router | RouterNameAlreadyExistsError | RouterAliasAlreadyExistsError:
132-
db_user_id = None if user_id == MASTER_USER_ID else user_id
132+
db_user_id = None if user_id == MASTER_ID else user_id
133133
aliases = aliases or []
134134

135135
try:

0 commit comments

Comments
 (0)