Skip to content

Commit 2a93f97

Browse files
authored
716 dissociate the encryption key from the api keys of the master password (#779)
* chore(env): add newline at the end of file # Conflicts: # .env.example * chore(docs): update inline documentation * feat(auth): add auth_secret_key setting - remove session_secret_key setting from configuration - remove default value for session_secret_key - remove constr() deprecated function to implement new Annotated syntax in configuration.py - add new auth_secret_key setting - deprecated auth_master_key setting (still available to use for a moment) - add default value for auth_secret_key (default = auth_master_key value for retrocompatibility) - add deprecation warning messages - fix EmptyDepencency typo to EmptyDependency - remove usage of auth_master_key in most of the codebase - add typing in IdentityAccessManager property - identify duplicate functions for token encoding (added TODO) - rename get_master_key into get_secret_key - update SessionMiddleware (cookie encoding/decoding) middleware to now use auth_secret_key - update config.yml, config.example.yml and config.test.yml - remove duplicate socket_keepalive in config.test.yml - remove TOKEN_PREFIX from Key(BaseModel) class due to a duplicate from IdentityAccessManager - remove useless comment - remove duplicate content in api.schemas.admin.providers and most of their references - migrate old Provider class calls to the new one (in api.infrastructure.fastapi.schemas.providers) - update some part of the documentation (not complete yet) # Conflicts: # api/schemas/admin/providers.py * 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 * feat(admin): force ID 0 for admin rol and user - add master permission in PermissionType (unused for the moment) - update inline documentation - remove useless prints * feat(roles): migrate PermissionType from old to new class (incomplete) - restrict the master user permissions to only MASTER instead of the whole list. * feat(alembic): add migration to add master permission to database - add newline in carbon footprint migration - fix inline doc typo - remove debug prints and add logger prints - check that the master role and user creation runs only at very first run * chore(IAM): add CheckTokenResult typing class to check_token method * chore(user): clean if statement in create_user method - update TODO comments to make them clearer * feat(admin): add MASTER permission bypass in is_admin method * feat(admin): add deletion restriction for master user - move MASTER_ID constant into variable.py - rename user into user_id in DELETE /user endpoint * chore(const): add typing to variable.py constants * feat(amdin): add restrictions for master user in role creation, role update, user creation and user update - needs to be tested * fix(token): fix false positive on InvalidAPIKeyException for Master user * feat(models): give access to models to master user * chore(permission): migrate permission type - from api.schemas.admin.roles to api.domain.role.entities * fix(alembic): fix migration history * fix(rebase): fix bad rebase conflict strategy - add typing for ecologit class * fix(rebase): revert bad python package update (mistral) * feat(admin): remove alembic migration as we do not want to create a master user anymore * chore: replace str, Enum inheritance by StrEnum type * feat(role, user): migrate some endpoints from role and user ressourcesto clean architecture - migrate endpoints - add use cases for role, user, and bootstrap - add exceptions * chore(test): commented tests because of usecase migration * feat(admin): migrate to new bootstrap admin management (without master user) - remove master role - add security boundaries to prevent deleting last admin user or admin role of OGL - add new bootstrap admin version - add new exceptions, repistories and factories - remove old endpoints - update default admin user and default admin password * chore(sql): remove empty session.py * chore: remove useless comments, update lifespan prototype * feat(admin): remove securities about admin self deletion. An admin can now delete himself - fix get_postgres_session iterator call - quick fix of Routers class - migrated to clean archi * fix(admin): fix obsolete admin bypass (before id 0, now permission ADMIN) - fix several playground bugs (login page glitch, redis token lock duration, migration issue on local, usage crash, 500 errors) - add shield badge on corresponding admin roles - add background tasks in playground * tests(admin): add some integration tests for admin bootstrap * tests(admin): add custom values for bootstrap admin tests * fix(provider): replace old Provider class by new one for rebase - still need to test * feat(playground): fix seletors - fix strategy display when updating a router * test(postgres): add context comments for has admin db tests * test(users): add test suit for get users repository - add temporary implementation of update user - add temporary implementation of delete user * test(admin, bootstrap, roles, users): add tests for role and user use cases - fix typing typo - clean unused bootstrap and hasadmin usecases - add some TODO comments - move use case tests to unit folder instead of integration folder * fix(playground): fix KeyError: 'carbon' in Usage page * fix(playground): fix selector blank display for router update and creation (load balancing strategy) * test(models): add new test on models when admin user don't have limits - fix some models tests * restore playground/ to main branch version * fix(tests): fix unit tests. working on integration tests
1 parent 655a8b1 commit 2a93f97

File tree

84 files changed

+2198
-562
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+2198
-562
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ REDIS_PASSWORD=changeme
1919
ELASTICSEARCH_HOST=elasticsearch
2020
ELASTICSEARCH_PORT=9200
2121
ELASTICSEARCH_USER=elasticsearch
22-
ELASTICSEARCH_PASSWORD=changeme
22+
ELASTICSEARCH_PASSWORD=changeme

api/alembic/versions/2025_12_22_1507-f02a2525b97c_remove_carbon_footprint_prefix.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import sqlalchemy as sa
1414
from sqlalchemy.dialects import postgresql
1515

16+
1617
# revision identifiers, used by Alembic.
1718
revision: str = 'f02a2525b97c'
1819
down_revision: Union[str, None] = '551b2920b23c'

api/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def _setup_sentry(configuration: Configuration) -> None:
5454

5555

5656
def _setup_middleware(app: FastAPI, configuration: Configuration) -> None:
57-
app.add_middleware(SessionMiddleware, secret_key=configuration.settings.session_secret_key)
57+
app.add_middleware(SessionMiddleware, secret_key=configuration.settings.auth_secret_key)
5858

5959
@app.middleware("http")
6060
async def set_request_context(request: Request, call_next):

api/dependencies.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66

77
from api.domain.key import KeyRepository
88
from api.infrastructure.model import ModelProviderGateway
9-
from api.infrastructure.postgres import PostgresKeyRepository, PostgresProviderRepository, PostgresRouterRepository, PostgresUserInfoRepository
9+
from api.infrastructure.postgres import (
10+
PostgresKeyRepository,
11+
PostgresProviderRepository,
12+
PostgresRolesRepository,
13+
PostgresRouterRepository,
14+
PostgresUserInfoRepository,
15+
PostgresUserRepository,
16+
)
1017
from api.schemas.core.context import RequestContext
1118
from api.use_cases.admin.providers import (
1219
CreateProviderUseCase,
@@ -15,7 +22,9 @@
1522
GetProvidersUseCase,
1623
UpdateProviderUseCase,
1724
)
25+
from api.use_cases.admin.roles import CreateRoleUseCase
1826
from api.use_cases.admin.routers import CreateRouterUseCase, DeleteRouterUseCase, GetOneRouterUseCase, GetRoutersUseCase, UpdateRouterUseCase
27+
from api.use_cases.admin.users import CreateUserUseCase
1928
from api.use_cases.models import GetModelsUseCase
2029
from api.utils.configuration import configuration
2130
from api.utils.context import global_context, request_context
@@ -25,8 +34,8 @@ def get_request_context() -> ContextVar[RequestContext]:
2534
return request_context
2635

2736

28-
def get_master_key() -> str:
29-
return configuration.settings.auth_master_key
37+
def get_secret_key() -> str:
38+
return configuration.settings.auth_secret_key
3039

3140

3241
# databases
@@ -44,6 +53,14 @@ async def get_postgres_session() -> AsyncGenerator[AsyncSession]:
4453

4554

4655
# repositories
56+
def _user_repository(session: AsyncSession) -> PostgresUserRepository:
57+
return PostgresUserRepository(postgres_session=session)
58+
59+
60+
def _role_repository(session: AsyncSession) -> PostgresRolesRepository:
61+
return PostgresRolesRepository(postgres_session=session)
62+
63+
4764
def _router_repository(session: AsyncSession) -> PostgresRouterRepository:
4865
return PostgresRouterRepository(postgres_session=session, app_title=configuration.settings.app_title)
4966

@@ -68,6 +85,16 @@ def get_models_use_case(
6885
)
6986

7087

88+
# users use cases
89+
def create_user_use_case_factory(postgres_session: AsyncSession = Depends(get_postgres_session)) -> CreateUserUseCase:
90+
return CreateUserUseCase(user_repository=_user_repository(postgres_session), user_info_repository=_user_info_repository(postgres_session))
91+
92+
93+
# roles use cases
94+
def create_role_use_case_factory(postgres_session: AsyncSession = Depends(get_postgres_session)) -> CreateRoleUseCase:
95+
return CreateRoleUseCase(role_repository=_role_repository(postgres_session), user_info_repository=_user_info_repository(postgres_session))
96+
97+
7198
# routers use cases
7299
def get_one_router_use_case_factory(postgres_session: AsyncSession = Depends(get_postgres_session)) -> GetOneRouterUseCase:
73100
return GetOneRouterUseCase(router_repository=_router_repository(postgres_session), user_info_repository=_user_info_repository(postgres_session))

api/domain/bootstrap/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class AdminAlreadyExistsError:
6+
pass

api/domain/key/entities.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from jose import JWTError, jwt
22
from pydantic import BaseModel, Field
33

4+
from api.helpers._identityaccessmanager import IdentityAccessManager
45
from api.utils.exceptions import InvalidAPIKeyException
56

6-
MASTER_USER_ID = 0
7-
MASTER_KEY_ID = 0
8-
97

108
class KeyClaims(BaseModel):
119
user_id: int
@@ -15,19 +13,18 @@ class KeyClaims(BaseModel):
1513
class Key(BaseModel):
1614
"""API Key entity"""
1715

18-
TOKEN_PREFIX: str = "sk-"
1916
value: str = Field(..., description="The raw API key value")
2017

21-
def decode(self, master_key: str) -> KeyClaims:
22-
if self.value == master_key:
23-
return KeyClaims(user_id=MASTER_USER_ID, key_id=MASTER_KEY_ID)
24-
25-
if not self.value.startswith(self.TOKEN_PREFIX):
18+
def decode(self, secret_key: str) -> KeyClaims:
19+
if not self.value.startswith(IdentityAccessManager.TOKEN_PREFIX):
2620
raise InvalidAPIKeyException()
2721

2822
try:
29-
jwt_token = self.value.split(self.TOKEN_PREFIX)[1]
30-
claims = jwt.decode(token=jwt_token, key=master_key, algorithms=["HS256"])
31-
return KeyClaims(user_id=claims["user_id"], key_id=claims["token_id"])
23+
# TODO: Refactor this. It's a duplicate with api.helpers._identityaccessmanager.IdentityAccessManager._decode_token
24+
jwt_token = self.value.split(IdentityAccessManager.TOKEN_PREFIX)[1]
25+
claims = jwt.decode(token=jwt_token, key=secret_key, algorithms=["HS256"])
26+
return KeyClaims(
27+
user_id=claims["user_id"], key_id=claims["token_id"]
28+
) # TODO: ensure token_id is included in claims when creating token, test the behavior when token_id is missing but the API key has the sk- format
3229
except (JWTError, IndexError):
3330
raise InvalidAPIKeyException()

api/domain/role/_rolerepository.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from abc import ABC, abstractmethod
22

3-
from api.domain.role.entities import Role
3+
from api.domain.role.entities import Limit, PermissionType, Role
4+
from api.domain.role.errors import RoleAlreadyExistsError
45

56

67
class RoleRepository(ABC):
78
@abstractmethod
8-
async def get_roles(self, role_id: str) -> list[Role]:
9+
async def create_role(self, name: str, permissions: list[PermissionType], limits: list[Limit]) -> Role | RoleAlreadyExistsError:
910
pass
1011

1112
@abstractmethod
12-
async def create_role(self, role: Role) -> Role:
13+
async def get_roles(self, role_id: str) -> list[Role]:
1314
pass
1415

1516
@abstractmethod

api/domain/role/entities.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import datetime as dt
2-
from enum import Enum
2+
from enum import StrEnum
33

44
from pydantic import BaseModel, Field
55

66

7-
class PermissionType(str, Enum):
7+
class PermissionType(StrEnum):
88
ADMIN = "admin"
99
CREATE_PUBLIC_COLLECTION = "create_public_collection"
1010
READ_METRIC = "read_metric"
1111
PROVIDE_MODELS = "provide_models"
1212

1313

14-
class LimitType(str, Enum):
14+
class LimitType(StrEnum):
1515
TPM = "tpm"
1616
TPD = "tpd"
1717
RPM = "rpm"

api/domain/role/errors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class RoleAlreadyExistsError:
6+
name: str
7+
8+
9+
@dataclass
10+
class RoleNotFoundError:
11+
name: str

0 commit comments

Comments
 (0)