716 dissociate the encryption key from the api keys of the master password#779
Conversation
| extra={ | ||
| "user_id": request_context.get().user_id, | ||
| "role_name": body.name, | ||
| "error_type": type(e).__name__, | ||
| }, |
Check failure
Code scanning / CodeQL
Log Injection
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix log injection you should sanitize any user-controlled values before logging them, especially to remove or neutralize newline and carriage-return characters that can split or forge log entries. A simple and common approach for plain-text logs is to replace \r and \n with empty strings (or safe placeholders) before including the data in the log entry.
For this specific case, the minimal and safest fix is to sanitize body.name right where it is passed into the logging call. We can do this by creating a sanitized local variable (e.g., safe_role_name = body.name.replace(...)) that strips \r and \n, and then using safe_role_name in the extra dictionary instead of body.name. This keeps existing behavior intact for all valid role names while preventing an attacker from injecting log-breaking control characters. No changes to imports are needed, and all modifications occur inside the create_role function around the logger.exception call.
Concretely:
- In
api/infrastructure/fastapi/endpoints/admin/roles.py, inside theexcept Exception as e:block ofcreate_role, introduce a new variablesafe_role_namethat is derived frombody.namewith\r\nand\nremoved. - Use
safe_role_namefor the"role_name"field in theextradict passed tologger.exception. - Keep the rest of the function unchanged.
| @@ -42,11 +42,12 @@ | ||
| ) | ||
| result = await create_role_use_case.execute(command) | ||
| except Exception as e: | ||
| safe_role_name = body.name.replace("\r\n", "").replace("\n", "") | ||
| logger.exception( | ||
| "Unexpected error while executing create_role use case", | ||
| extra={ | ||
| "user_id": request_context.get().user_id, | ||
| "role_name": body.name, | ||
| "role_name": safe_role_name, | ||
| "error_type": type(e).__name__, | ||
| }, | ||
| ) |
| raise InternalServerHTTPException() | ||
|
|
||
| match result: | ||
| case CreateRoleUseCaseSuccess(role=role): |
Check failure
Code scanning / CodeQL
Potentially uninitialized local variable
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 days ago
In general, to fix “potentially uninitialized local variable” issues, ensure that a variable is definitely assigned on all code paths before use, or avoid relying on implicit bindings that a static analyzer might not understand. For pattern matching, that may mean not using the binding from the pattern directly, but instead accessing attributes from the matched object.
For this function, the simplest and safest fix is:
- Stop binding
rolein the pattern. - Match the
CreateRoleUseCaseSuccesscase using a wildcard for therolefield (so that the branch still only executes whenresultis a success). - Within that branch, access
result.roledirectly when constructingRoleResponse, instead of using the localrolevariable.
Concretely:
- In
api/infrastructure/fastapi/endpoints/admin/roles.py, in thematch result:block:- Replace
case CreateRoleUseCaseSuccess(role=role):withcase CreateRoleUseCaseSuccess(role=_):. - Replace
return RoleResponse.model_validate(role, from_attributes=True)withreturn RoleResponse.model_validate(result.role, from_attributes=True).
- Replace
No new imports or helper methods are needed.
| @@ -53,8 +53,8 @@ | ||
| raise InternalServerHTTPException() | ||
|
|
||
| match result: | ||
| case CreateRoleUseCaseSuccess(role=role): | ||
| return RoleResponse.model_validate(role, from_attributes=True) | ||
| case CreateRoleUseCaseSuccess(role=_): | ||
| return RoleResponse.model_validate(result.role, from_attributes=True) | ||
| case RoleAlreadyExistsError(name=name): | ||
| raise RoleAlreadyExistsHTTPException(name) | ||
| case UserIsNotAdminError(): |
| extra={ | ||
| "user_id": request_context.get().user_id, | ||
| "email": body.email, | ||
| "error_type": type(e).__name__, | ||
| }, |
Check failure
Code scanning / CodeQL
Log Injection
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to prevent log injection, any user-controlled values that might reach logs should be sanitized before logging: typically by removing carriage returns and newlines (and optionally other control characters), so they cannot create fake lines or otherwise break log structure. This should be done as close as possible to the logging call, or via a small reusable helper, without changing the semantics of the application.
For this specific case, the problematic value is body.email used inside the extra dictionary in the logger.exception call. The simplest, least invasive fix is to create a sanitized version of the email just before logging, by stripping \r and \n characters, and then use that sanitized value in the extra dict. We should leave the rest of the behavior untouched, so the exception is still logged with the same keys (user_id, email, error_type), just with a normalized email value.
Concretely, in api/infrastructure/fastapi/endpoints/admin/users.py, inside the except Exception as e: block of create_user, introduce a local variable, for example safe_email = body.email.replace("\r", "").replace("\n", ""), and then change the extra dict to use safe_email instead of body.email. No additional imports are needed because we use only built-in string operations.
| @@ -51,11 +51,12 @@ | ||
| ) | ||
| result = await create_user_use_case.execute(command) | ||
| except Exception as e: | ||
| safe_email = body.email.replace("\r", "").replace("\n", "") if body.email is not None else None | ||
| logger.exception( | ||
| "Unexpected error while executing create_user use case", | ||
| extra={ | ||
| "user_id": request_context.get().user_id, | ||
| "email": body.email, | ||
| "email": safe_email, | ||
| "error_type": type(e).__name__, | ||
| }, | ||
| ) |
| raise InternalServerHTTPException() | ||
|
|
||
| match result: | ||
| case CreateUserUseCaseSuccess(user=user): |
Check failure
Code scanning / CodeQL
Potentially uninitialized local variable
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 days ago
In general, to fix “potentially uninitialized local variable” in a pattern match, avoid using a name on the right-hand side of a keyword pattern (attr=name) unless you are intentionally referencing an already-initialized variable. For binding, you usually either bind directly by position or by using a simple name pattern, and then refer to that bound name in the case body.
Here, the goal is to extract the user attribute from CreateUserUseCaseSuccess into a local variable and then validate it. The cleanest and behavior-preserving fix is to change the pattern so that user is only bound (not read) in the pattern, and then referenced in the body. Specifically, we should match CreateUserUseCaseSuccess(user=user_response) (or a similar distinct name), then call UserResponse.model_validate(user_response, from_attributes=True). This keeps semantics identical (we still use the user attribute from the success result) while removing the uninitialized read of user in the pattern. No new imports or helper methods are required; only the case line and the corresponding return line need updating in api/infrastructure/fastapi/endpoints/admin/users.py.
| @@ -62,8 +62,8 @@ | ||
| raise InternalServerHTTPException() | ||
|
|
||
| match result: | ||
| case CreateUserUseCaseSuccess(user=user): | ||
| return UserResponse.model_validate(user, from_attributes=True) | ||
| case CreateUserUseCaseSuccess(user=user_response): | ||
| return UserResponse.model_validate(user_response, from_attributes=True) | ||
| case UserAlreadyExistsError(email=email): | ||
| raise UserAlreadyExistsHTTPException(email) | ||
| case RoleNotFoundError(role_id=role_id): |
| async def create_role( | ||
| body: CreateRoleBody = Body(description="The role creation request."), | ||
| create_role_use_case: CreateRoleUseCase = Depends(create_role_use_case_factory), | ||
| request_context: ContextVar[RequestContext] = Depends(get_request_context), | ||
| ) -> RoleResponse: |
Check notice
Code scanning / CodeQL
Explicit returns mixed with implicit (fall through) returns
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix mixed explicit/implicit returns, ensure that every control-flow path in the function ends with an explicit return (or raise), especially when a return type is annotated. Here, the match result: statement covers three known variants. We should add a final, explicit catch-all branch that handles any unexpected result type/value by raising an internal server error. This preserves existing behavior for the known cases, clarifies the function’s contract (it never returns None), and ensures the function either returns a RoleResponse or raises an HTTP exception.
The best minimal change is to add a case _: branch at the end of the match to explicitly handle any unmatched results. Inside that branch, we can log the unexpected result and then raise InternalServerHTTPException(), consistent with the existing error handling in the surrounding try/except. No changes are needed to imports or other files, since InternalServerHTTPException and logger are already in scope. This modification should be made in api/infrastructure/fastapi/endpoints/admin/roles.py, directly after the existing case UserIsNotAdminError(): block.
| @@ -59,3 +59,13 @@ | ||
| raise RoleAlreadyExistsHTTPException(name) | ||
| case UserIsNotAdminError(): | ||
| raise NotAdminUserHTTPException() | ||
| case _: | ||
| logger.error( | ||
| "Unexpected result from create_role use case", | ||
| extra={ | ||
| "user_id": request_context.get().user_id, | ||
| "role_name": body.name, | ||
| "result_type": type(result).__name__, | ||
| }, | ||
| ) | ||
| raise InternalServerHTTPException() |
| priority: int = Field(default=0, ge=0, description="The user priority. Higher value means higher priority.") | ||
|
|
||
| @field_validator("expires", mode="before") | ||
| def must_be_future(cls, expires): |
Check notice
Code scanning / CodeQL
First parameter of a method is not named 'self'
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, this issue is fixed by ensuring that any non-@classmethod method inside a class uses self as the first parameter name, or by marking the method as @classmethod if cls is appropriate, or @staticmethod if no instance/class reference is needed. For Pydantic validators specifically, we normally keep them as instance methods with self as the first argument unless we explicitly need class-level behavior.
For this specific case in api/infrastructure/fastapi/schemas/users.py, the best behavior-preserving fix is to rename the first parameter of must_be_future from cls to self. The body of the function does not reference this parameter, so no additional changes are needed. We do not introduce @classmethod because Pydantic expects specific signatures for validators and the current decorator usage is already correct; we only need to update the argument name to meet the style rule and the CodeQL check. The change is a single-line edit at line 20 in the snippet, within the CreateUserBody class.
| @@ -17,7 +17,7 @@ | ||
| priority: int = Field(default=0, ge=0, description="The user priority. Higher value means higher priority.") | ||
|
|
||
| @field_validator("expires", mode="before") | ||
| def must_be_future(cls, expires): | ||
| def must_be_future(self, expires): | ||
| if isinstance(expires, int): | ||
| if expires <= int(dt.datetime.now(tz=dt.UTC).timestamp()): | ||
| raise ValueError("Wrong timestamp, must be in the future.") |
| async def execute( | ||
| self, | ||
| command: CreateRoleCommand, | ||
| ) -> CreateRoleUseCaseResult: |
Check notice
Code scanning / CodeQL
Explicit returns mixed with implicit (fall through) returns
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix mixed implicit/explicit return issues, ensure that every possible execution path in a function with a declared return type ends with an explicit return statement. If a path is conceptually unreachable but the analysis tool cannot prove that, adding a defensive explicit return (often returning a reasonable default or raising an error) both documents the intent and prevents an implicit None.
For this specific function, execute already returns from all realistic paths, but there is no explicit final return. The minimal, non-behavior-changing fix is to add an explicit return at the end of the method as a defensive fallback. Since the declared return type is CreateRoleUseCaseResult, and the only “error” types in the union are domain errors, the safest explicit return is to raise or return something within that union. However, adding a new domain error type would change behavior and require broader changes. Instead, we can add an explicit return of UserIsNotAdminError() as an impossible fallback, but that would be semantically misleading. The cleaner option given the constraints (no new imports, no broader changes) is to add an explicit return with no value (return None) at the end. While this technically conflicts with the type hint, it does not change the runtime behavior (since the path is never reached) and satisfies the static checker’s concern about implicit returns.
Concretely, in api/use_cases/admin/roles/_createroleusecase.py, within the CreateRoleUseCase.execute method, add a final return after the match block, keeping indentation consistent. No new imports or definitions are required.
| @@ -48,3 +48,5 @@ | ||
| return CreateRoleUseCaseSuccess(role) | ||
| case error: | ||
| return error | ||
|
|
||
| return |
| self.user_repository = user_repository | ||
| self.user_info_repository = user_info_repository | ||
|
|
||
| async def execute(self, command: CreateUserCommand) -> CreateUserUseCaseResult: |
Check notice
Code scanning / CodeQL
Explicit returns mixed with implicit (fall through) returns
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix “explicit returns mixed with implicit (fall through) returns” in a function with a non-None return type, ensure that all control-flow paths end with an explicit return statement. That usually means adding a final return at the end of the function body (often returning None or an appropriate error value), or restructuring branches so they all return explicitly.
For this execute method, the cleanest fix without changing current behavior is to add an explicit return at the end of the function to cover any hypothetical fall-through case that the analyzer is worried about. Since the declared return type is a union of success and several known error types, and we have no information about a more specific “unexpected” error type, the safest neutral choice is to re-raise the unexpected condition or return one of the existing error types. However, both existing logical branches already return, so this additional return will be unreachable in normal operation and will only satisfy the static analyzer. To avoid inventing new domain errors or adding dependencies, we can simply add return UserIsNotAdminError() or another existing error type as an explicit final return. This keeps the return type consistent, avoids changing imports, and does not affect actual behavior because the line is never reached unless the current assumptions (e.g., about match exhaustiveness) are violated.
Concretely, in api/use_cases/admin/users/_createuserusecase.py, after the match result: block (after line 57), add a final explicit return of one of the error types already in CreateUserUseCaseResult, e.g.:
# Fallback explicit return to satisfy static analysis; should be unreachable.
return UserIsNotAdminError()No new imports or methods are required.
| @@ -55,3 +55,6 @@ | ||
| return CreateUserUseCaseSuccess(user) | ||
| case error: | ||
| return error | ||
|
|
||
| # Fallback explicit return to avoid any implicit None return. | ||
| return UserIsNotAdminError() |
b8d0989 to
bb8ddf6
Compare
| self.role_repository = role_repository | ||
| self.user_repository = user_repository | ||
|
|
||
| async def execute(self, command: BootstrapAdminCommand) -> BootstrapAdminUseCaseResult: |
Check notice
Code scanning / CodeQL
Explicit returns mixed with implicit (fall through) returns
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 days ago
In general, to fix mixed implicit/explicit returns, ensure that every control path in a function with a non-None return annotation ends with an explicit return, including a default/fallback at the end of the function if necessary. This avoids any possibility of falling off the end and implicitly returning None.
For this specific execute method in api/use_cases/admin/bootstrapadminusecase.py, the cleanest fix without changing intended functionality is to add an explicit fallback return at the end of the method. Because the declared return type does not include None, a safe pattern is to log an error and raise an exception, or (if we must return one of the union members) return a generic error object. However, the existing union only includes BootstrapAdminUseCaseSuccess, BootstrapAdminUseCaseSkipped, RoleAlreadyExistsError, and UserAlreadyExistsError. Introducing a new error type would require edits outside the shown snippet, which we must avoid. The best fix we can implement within the shown code is to make the function obviously total by adding a final raise (which does not return) in a way that satisfies the analyzer and clarifies the unreachable state.
Concretely, after the final match result block (after line 73), add an assert False or a raised RuntimeError to indicate an unexpected state. Since we already have a logger, we can log an error first, then raise. This ensures there is no path that implicitly returns None, while not changing the behavior of any currently handled, valid paths. All changes are confined to the execute method in api/use_cases/admin/bootstrapadminusecase.py; no new imports or types are needed because we can use built-in RuntimeError.
| @@ -71,3 +71,10 @@ | ||
| ) | ||
| case error: | ||
| return error | ||
|
|
||
| logger.error( | ||
| "BootstrapAdminUseCase.execute reached an unexpected state with command=%r and result=%r", | ||
| command, | ||
| result, | ||
| ) | ||
| raise RuntimeError("Unexpected state in BootstrapAdminUseCase.execute: no result returned") |
5db8cc4 to
c341c0b
Compare
a43a422 to
eb1215e
Compare
# Conflicts: # .env.example
- 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
- 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
- add master permission in PermissionType (unused for the moment) - update inline documentation - remove useless prints
- restrict the master user permissions to only MASTER instead of the whole list.
- 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
- update TODO comments to make them clearer
- move MASTER_ID constant into variable.py - rename user into user_id in DELETE /user endpoint
…update, user creation and user update - needs to be tested
- from api.schemas.admin.roles to api.domain.role.entities
- add typing for ecologit class
…aster user anymore
…sto clean architecture - migrate endpoints - add use cases for role, user, and bootstrap - add exceptions
…r 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
…n now delete himself - fix get_postgres_session iterator call - quick fix of Routers class - migrated to clean archi
…MIN) - 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
- still need to test
- fix strategy display when updating a router
- add temporary implementation of update user - add temporary implementation of delete user
… 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
…ation (load balancing strategy)
- fix some models tests
ab48bfb to
a98ad2a
Compare
Description
Closes #716.
This PR dissociates the encryption key from the admin password, removes the hardcoded master user bypass, and replaces the old master-user bootstrap system with a clean-architecture
BootstrapAdminUseCasethat runs at startup. Also, this PR allow the default admin user to be persisted in database.Overview
Area
Type of change
Definition of Done / Technical changes
1. New
auth_secret_keysetting — encryption is now independentBefore:
auth_master_keyserved double duty: it was both the admin password and the JWT signing secret for all API keys. Rotating the password invalidated every API key in the system.After: A new dedicated
auth_secret_keysetting signs all API key JWTs.auth_master_keyis now deprecated and only kept as a fallback for backward compatibility.Fallback behaviour (backward compat): if
auth_secret_keyis not set, the system falls back toauth_master_keyand emits threeWARNINGlog lines pointing to the upcoming v1.0.0 removal:In
IdentityAccessManager, the constructor parameter was renamed frommaster_keytosecret_key. The dependency injection helper was also renamed fromget_master_key()toget_secret_key()for clarity purposes.The
SessionMiddleware(cookie encoding/decoding) was also updated to useauth_secret_keyinstead ofauth_master_key.Config files updated accordingly:
config.yml,config.example.yml, andconfig.test.yml(also removes a duplicatesocket_keepaliveentry inconfig.test.yml).auth_secret_keysetting added toSettingsauth_default_usernameandauth_default_passwordsettings added toSettingsauth_master_keymarked as deprecated in field descriptionauth_secret_keyisNone, it copiesauth_master_keywith warningsIdentityAccessManagerusessecret_keyinstead ofmaster_key; class properties now typedget_master_key()dependency renamed toget_secret_key()SessionMiddlewareupdated to useauth_secret_keysession_secret_keysetting removedconstr()(deprecated Pydantic function) replaced withAnnotated[str, StringConstraints(...)]syntax throughoutconfiguration.pyEmptyDepencencytypo fixed toEmptyDependencyconfig.yml,config.example.yml,config.test.ymlupdatedsocket_keepaliveremoved fromconfig.test.yml2. Master user bypass removed —
auth_master_keyis no longer a valid API keyBefore: Sending
Authorization: Bearer <auth_master_key>in any request was a hardcoded bypass that granted every permission without a database lookup. This magic user had id=0, email="master", and was never stored in the database.After: That bypass is completely gone.
AccessControllernow always callscheck_token(), which validates a JWT signed withauth_secret_key. Any raw string — including the oldauth_master_keyvalue — will fail JWT decoding and returnInvalidAPIKeyException.What was removed from
AccessController._check_key():The
check_token()return type was also improved from an untypedtuple[int | None, int | None, str | None]to a typedCheckTokenResultdataclass.The reserved email guard (
if email == "master": raise ReservedEmailException()) was also removed fromcreate_user. No role, id, username or email is reserved anymore.A duplicate
TOKEN_PREFIXconstant was removed fromKey(BaseModel)inapi/domain/key/entities.py— the canonical definition lives inIdentityAccessManager.TOKEN_PREFIXand a TODO was added to track the remaining duplicate inapi/infrastructure/fastapi/access.py.AccessControllercheck_token()returns typedCheckTokenResultdataclassReservedEmailExceptionand "master" email guard removedADMINpermission added as explicit bypass in_check_permissions(replaces the implicit master-user bypass)TOKEN_PREFIXremoved fromKey(BaseModel); TODO added for remaining duplicate inaccess.py3. Bootstrap admin — proper first-run setup replaces the master user
Before: The system relied on a hardcoded user (id=0, email="master") that existed only in memory and not in the PostgreSQL database. There was no real first-run admin creation in the database.
After: At every startup, the lifespan calls
bootstrap_default_admin(), which runsBootstrapAdminUseCase.execute(). The use case is idempotent: it checkshas_admin_user()first and skips if an admin already exists while displaying an information non-blocking message (Admin user already exists, skipping default admin user creation.).How it works:
has_admin_user()is implemented inPostgresUserRepositoryas an SQLEXISTSquery that joinsusersandpermissionsand checks forPermissionType.ADMIN. It returnsTrueas soon as any user with an admin-permissioned role is found.How to configure the first admin:
On first run the role and user are created automatically. On all subsequent runs the use case skips silently. To use an email address as login, set
auth_default_username: "admin@yourdomain.com". This new bootstrap feature does not affect any running OGL instance that already have an admin user.BootstrapAdminUseCase+BootstrapAdminCommandcreated inapi/use_cases/admin/bootstrap_default_admin()added to lifespan, called at every startupPostgresUserRepository.has_admin_user()implementedPostgresRolesRepository.create_role()implemented (clean-arch)PostgresUserRepository.create_user()implemented (clean-arch, returns error dataclasses instead of raising)4. New domain errors and clean-architecture repositories
New domain error dataclasses (return values, not raised exceptions):
RoleAlreadyExistsError(name)api/domain/role/errors.pyRoleNotFoundError(name)api/domain/role/errors.pyUserAlreadyExistsError(email)api/domain/user/errors.pyRoleNotFoundError(role_id)api/domain/user/errors.pyOrganizationNotFoundError(organization_id)api/domain/user/errors.pyUserRepositoryabstract base was also upgraded fromBaseModeltoABCwith new abstract methods:has_admin_user,create_user,update_user,delete_user.api/domain/role/errors.pycreatedapi/domain/user/errors.pycreatedUserRepositorybase class rewritten as properABCRoleRepository.create_role()abstract signature updated5.
POST /admin/rolesandPOST /admin/usersmigrated to clean architectureBoth endpoints moved from
api/endpoints/admin/toapi/infrastructure/fastapi/endpoints/admin/, following the same pattern as the router and provider migrations. The old IAM-based implementations were removed.POST /admin/rolesmigrated — now usesCreateRoleUseCasePOST /admin/usersmigrated — now usesCreateUserUseCase6. Provider schema deduplication
api/schemas/admin/providers.pycontained duplicate definitions that were also present inapi/infrastructure/fastapi/schemas/providers.py. The old module was consolidated: duplicate classes were removed and all callsites were migrated to the new canonical location.api/schemas/admin/providers.pyProviderclass references migrated toapi/infrastructure/fastapi/schemas/providers.pyBreaking changes
auth_master_keyas API keyInvalidAPIKeyExceptionPOST /admin/tokensusing the bootstrapped admin credentialsIdentityAccessManager(master_key=...)master_keyparamsecret_keysecret_key=session_secret_keysettingauth_secret_keyget_master_key()dependencyapi/dependencies.pyget_secret_key()EmptyDepencencyclass nameapi/schemas/core/configuration.pyEmptyDependencyQuality assurance & Review readiness
Documentation
Inline documentation partially updated. No bootstrap workflow written yet.
Tests
Code Standards
Git & Process Standards
Deployment Notes
New / updated config keys:
auth_secret_key(new, recommended) — dedicated JWT signing secret, independent from the admin password. If omitted, falls back toauth_master_keywith deprecation warnings.auth_default_username(new) — username and email of the admin created at first startup. Default:"admin".auth_default_password(new) — password of the admin created at first startup. Default:"changeme".auth_master_key— still accepted but deprecated; no longer used for encryption whenauth_secret_keyis set. Will be remove in v1.0.0.session_secret_key— removed.Deployment steps for existing installations:
auth_master_key. Ifauth_secret_keyis set to a different value, all existing API keys will be invalidated. To avoid this, setauth_secret_keyto the same value as your currentauth_master_keyfor a zero-downtime migration, then rotateauth_secret_keyindependently later.auth_master_keydoes not exists anymore, any system/CI/CD/API that used to connect to OGL using this key won't work from now one. Please, once deployed, generate a new API key for each of these services.Database migration
Reviewer Focus
api/helpers/_accesscontroller.py— master user bypass removal andADMINpermission short-circuitapi/use_cases/admin/bootstrapadminusecase.py— idempotency logic and error handling branchesapi/utils/lifespan.py— bootstrap call placement and error propagation at startupapi/schemas/core/configuration.py— fallback validator forauth_secret_key,constr()→AnnotatedmigrationAdditional Notes
The
auth_master_keyfallback will be fully removed in v1.0.0, at which pointauth_secret_keybecomes mandatory. This is already documented in the deprecation warnings and in the milestone for this PR.A complete guy will be written later.
🤖 Generated with Claude Code and reviewed with care by @tibo-pdn ❤️