Skip to content

Conversation

@avirajsingh7
Copy link
Collaborator

@avirajsingh7 avirajsingh7 commented Oct 6, 2025

Summary

This PR refactors the API key authentication system to improve security by replacing encryption-based key storage with hash-based verification. The new implementation uses a prefix-based lookup strategy combined with secure hashing, while maintaining backward compatibility with existing encrypted keys.

Key Changes

Security Improvements

  • Hash-based storage: API keys are now hashed instead of encrypted, preventing key retrieval and enhancing security
  • Prefix-based lookup: Keys consist of a prefix (for database lookup) and secret part (for verification)
  • Backward compatibility: Support for verifying old encrypted API keys during transition period

Architecture Updates

  • APIKeyManager class: Replaced verify_api_key function with a comprehensive APIKeyManager class for better code organization
  • Permission system: Added permissions.py to separate authorization logic from authentication
  • Dependency refactoring: Updated deps.py to focus solely on authentication, with permission checks delegated to the new permission module

Database & Migration

  • Migration script: Added migrate_api_key.py to convert existing encrypted keys to hashed format and use uuid as primary key instead of int in APIkey table
  • Schema updates: Updated API key model and database schema to support the new authentication logic
  • Delete endpoint: Added API key deletion route with proper permission checks

Testing Infrastructure

  • AuthContext pattern: Refactored test cases to use AuthContext from seed data instead of generating keys (since hashed keys cannot be retrieved)
  • Seed data integration: Updated test utilities to leverage seed_data.json for consistent API key testing
  • Test coverage: Updated all affected test suites across collections, documents, assistants, credentials, and conversations

Summary by CodeRabbit

  • New Features

    • Create API keys (raw key shown once), list (masked) and delete per project with pagination
    • Unified auth context supporting API keys or JWTs and permission-based route guards
  • Improvements

    • API keys migrated to a stronger hashed format with stable prefixes and UUID identifiers for improved security
    • Centralized API key manager and clearer error responses; raw key disclosure only at creation
  • Tests

    • Expanded tests and test utilities covering auth, permissions, API key flows, migration, and verification

@avirajsingh7 avirajsingh7 self-assigned this Oct 6, 2025
@avirajsingh7 avirajsingh7 added the enhancement New feature or request label Oct 6, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 6, 2025

Walkthrough

Refactors API key storage and auth: introduces APIKeyManager (generate/verify with bcrypt), APIKeyCrud, AuthContext and permission checks, migrates encrypted keys to prefix+hash with optional UUID PK via Alembic, and updates tests/fixtures to use TestAuthContext.

Changes

Cohort / File(s) Summary
Migration & Alembic
backend/app/alembic/migrate_api_key.py, backend/app/alembic/versions/e7c68e43ce6f_refactor_api_key_table.py
New migration utility and Alembic revision that decrypts/parses old keys, bcrypt-hashes secrets, optionally generates UUIDs (new_id), populates key_prefix/key_hash, verifies migration, then converts schema to UUID primary key and updates indexes/columns.
Core security & models
backend/app/core/security.py, backend/app/models/auth.py, backend/app/models/api_key.py
Adds APIKeyManager and singleton api_key_manager (generate/verify supporting old/new formats) and new AuthContext model. Updates APIKey model to UUID id, key_prefix, key_hash, and adds APIKeyCreateResponse public model.
CRUD & onboarding
backend/app/crud/api_key.py, backend/app/crud/onboarding.py, backend/app/crud/__init__.py
Adds APIKeyCrud (project-scoped create/read_one/read_all/delete); onboarding uses api_key_manager.generate() and stores prefix/hash; crud exports now include APIKeyCrud and api_key_manager, removing legacy per-function exports.
API deps, permissions & routes
backend/app/api/deps.py, backend/app/api/permissions.py, backend/app/api/routes/api_keys.py
Adds get_auth_context dependency returning AuthContext and AuthContextDep; introduces Permission enum, has_permission, require_permission; API key routes rewritten to use AuthContextDep, SessionDep, and APIKeyCrud with new responses (raw key only on create, masked list, UUID ids).
Model relationships
backend/app/models/organization.py, backend/app/models/project.py, backend/app/models/user.py
Removed api_keys Relationship fields from Organization, Project, and User (drops back_populates and cascade_delete linkage).
Seed data & helpers
backend/app/seed_data/seed_data.py, backend/app/seed_data/seed_data.json
Seed parsing updated to compute/store key_prefix/key_hash instead of encrypted single key; JSON _comment added; created_at removed from APIKey seed entries.
Tests — fixtures, utils & test-data
backend/app/tests/conftest.py, backend/app/tests/utils/auth.py, backend/app/tests/utils/test_data.py, backend/app/tests/utils/utils.py, backend/app/tests/utils/document.py, backend/app/tests/utils/collection.py
New TestAuthContext and helpers (get_test_auth_context, get_superuser_test_auth_context, get_user_test_auth_context); fixtures now return TestAuthContext; create_test_api_key uses APIKeyCrud and returns APIKeyCreateResponse; removed legacy API-key lookup helpers.
Tests — routes & suites
backend/app/tests/api/routes/*, backend/app/tests/crud/*, backend/app/tests/core/*, backend/app/tests/api/*
Many tests updated to use TestAuthContext; added tests for APIKeyManager, APIKeyCrud, get_auth_context, and permissions; endpoint expectations adjusted (UUID id, key_prefix, raw key only on create).
Exports
backend/app/models/__init__.py, backend/app/crud/__init__.py
Public exports updated: AuthContext, APIKeyCreateResponse, APIKeyCrud, and api_key_manager added; older per-function API-key exports removed.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Route as Route Handler
    participant Deps as get_auth_context
    participant Mgr as APIKeyManager
    participant DB as Database

    Client->>Route: Request (Authorization / X-API-KEY)
    Route->>Deps: resolve auth dependency
    Deps->>Mgr: api_key_manager.verify(session, api_key)
    Mgr->>DB: SELECT apikey JOIN user, project, org
    DB-->>Mgr: APIKey + relations
    Mgr->>Mgr: parse prefix & secret → bcrypt verify
    alt verified
        Mgr-->>Deps: AuthContext
        Deps-->>Route: AuthContext
        Route->>Route: require_permission / handler
        Route-->>Client: 2xx response
    else invalid
        Mgr-->>Deps: None
        Deps-->>Route: raise HTTPException(401)
        Route-->>Client: 401 response
    end
Loading
sequenceDiagram
    participant Test
    participant Crud as APIKeyCrud
    participant Mgr as APIKeyManager
    participant DB as Database

    Test->>Crud: create(user_id, project_id)
    Crud->>Mgr: api_key_manager.generate()
    Mgr->>Mgr: build raw_key ("ApiKey "+prefix+secret)
    Mgr->>Mgr: bcrypt.hash(secret)
    Mgr-->>Crud: (raw_key, key_prefix, key_hash)
    Crud->>DB: INSERT APIKey (uuid id, key_prefix, key_hash, project_id, organization_id)
    DB-->>Crud: APIKey
    Crud-->>Test: (raw_key, APIKey)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • AkhileshNegi
  • kartpop

Poem

🐰 I hopped through keys both old and new,

Split prefixes, hashed secrets true;
UUIDs lined up in tidy rows,
AuthContext guards where every token goes;
A carrot for safety — nudge, nibble, chew!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Authentication: Replace encryption with hashing and add permission-based authorization" directly and specifically summarizes the two primary transformations in this changeset. The first part addresses the core technical change: replacing encrypted API key storage with bcrypt-based hashing (implemented via the new APIKeyManager class and migration logic). The second part accurately describes the new authorization system introduced through the Permission enum and permission-checking middleware in backend/app/api/permissions.py. The title is concise, avoids vague language, and provides clear insight into the authentication and authorization refactoring work without requiring further explanation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/authentication

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@avirajsingh7 avirajsingh7 linked an issue Oct 6, 2025 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
backend/app/crud/onboarding.py (1)

99-99: Consider using organization.id directly for consistency.

Line 99 uses project.organization_id, but organization is already available in scope (created or retrieved earlier). Using organization.id would be more direct and consistent with Line 110 where organization.id is used for credentials.

Apply this diff for consistency:

     api_key = APIKey(
         key_prefix=key_prefix,
         key_hash=key_hash,
         user_id=user.id,
-        organization_id=project.organization_id,
+        organization_id=organization.id,
         project_id=project.id,
     )
backend/app/tests/utils/collection.py (1)

32-32: Capture or document the ignored return value.

APIKeyCrud.create() returns a tuple (raw_key, api_key) (as shown in backend/app/crud/api_key.py lines 180-185), but the return value is not captured here. If the raw key is not needed for this test utility, consider adding a comment to clarify the intentional discard. Otherwise, if the key should be used, capture and return it.

If intentional, add a clarifying comment:

     # Step 2: Create API key for user with valid foreign keys
+    # Note: raw_key is not needed for collection setup
     APIKeyCrud(session=db, project_id=project.id).create(user_id=owner_id)

Or if the return value should be used:

-    APIKeyCrud(session=db, project_id=project.id).create(user_id=owner_id)
+    raw_key, api_key = APIKeyCrud(session=db, project_id=project.id).create(user_id=owner_id)
backend/app/tests/core/doctransformer/test_service/conftest.py (1)

55-63: Drop the unused db fixture argument

current_user no longer touches db, so keeping it triggers Ruff ARG001 and adds noise. Please remove the parameter (the user_api_key fixture already depends on db) or rename it to _db if you want to keep the dependency wiring.

backend/app/tests/utils/auth.py (1)

63-68: Use is_(False) for SQLAlchemy booleans.

Ruff flagged this equality check. Switching to .is_(False) avoids E712 and matches SQLAlchemy style.

Apply this diff:

-        .where(APIKey.is_deleted == False)
+        .where(APIKey.is_deleted.is_(False))
backend/app/tests/utils/utils.py (1)

85-94: Align boolean filter with SQLAlchemy style.

Replace the == False comparison with .is_(False) to satisfy Ruff’s E712 warning and adhere to SQLAlchemy conventions.

Apply this diff:

-    filters = [Assistant.is_deleted == False]
+    filters = [Assistant.is_deleted.is_(False)]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90e5bb1 and 86bf44b.

📒 Files selected for processing (32)
  • backend/app/alembic/migrate_api_key.py (1 hunks)
  • backend/app/alembic/versions/d209cddac1fa_refactor_api_key_table.py (1 hunks)
  • backend/app/api/deps.py (5 hunks)
  • backend/app/api/permissions.py (1 hunks)
  • backend/app/api/routes/api_keys.py (1 hunks)
  • backend/app/crud/__init__.py (1 hunks)
  • backend/app/crud/api_key.py (1 hunks)
  • backend/app/crud/onboarding.py (2 hunks)
  • backend/app/models/__init__.py (2 hunks)
  • backend/app/models/api_key.py (2 hunks)
  • backend/app/models/organization.py (0 hunks)
  • backend/app/models/project.py (0 hunks)
  • backend/app/models/user.py (1 hunks)
  • backend/app/seed_data/seed_data.json (1 hunks)
  • backend/app/seed_data/seed_data.py (3 hunks)
  • backend/app/tests/api/routes/collections/test_collection_info.py (4 hunks)
  • backend/app/tests/api/routes/collections/test_create_collections.py (4 hunks)
  • backend/app/tests/api/routes/documents/test_route_document_upload.py (2 hunks)
  • backend/app/tests/api/routes/test_api_key.py (0 hunks)
  • backend/app/tests/api/routes/test_assistants.py (16 hunks)
  • backend/app/tests/api/routes/test_creds.py (17 hunks)
  • backend/app/tests/api/routes/test_doc_transformation_job.py (15 hunks)
  • backend/app/tests/api/routes/test_openai_conversation.py (14 hunks)
  • backend/app/tests/conftest.py (2 hunks)
  • backend/app/tests/core/doctransformer/test_service/conftest.py (2 hunks)
  • backend/app/tests/crud/test_api_key.py (0 hunks)
  • backend/app/tests/crud/test_onboarding.py (0 hunks)
  • backend/app/tests/utils/auth.py (1 hunks)
  • backend/app/tests/utils/collection.py (2 hunks)
  • backend/app/tests/utils/document.py (3 hunks)
  • backend/app/tests/utils/test_data.py (2 hunks)
  • backend/app/tests/utils/utils.py (2 hunks)
💤 Files with no reviewable changes (5)
  • backend/app/tests/api/routes/test_api_key.py
  • backend/app/tests/crud/test_api_key.py
  • backend/app/tests/crud/test_onboarding.py
  • backend/app/models/organization.py
  • backend/app/models/project.py
🧰 Additional context used
🧬 Code graph analysis (24)
backend/app/tests/utils/collection.py (1)
backend/app/crud/api_key.py (1)
  • APIKeyCrud (139-236)
backend/app/crud/__init__.py (1)
backend/app/crud/api_key.py (1)
  • APIKeyCrud (139-236)
backend/app/alembic/versions/d209cddac1fa_refactor_api_key_table.py (1)
backend/app/alembic/migrate_api_key.py (2)
  • migrate_api_keys (34-154)
  • verify_migration (157-198)
backend/app/tests/utils/test_data.py (2)
backend/app/crud/api_key.py (1)
  • APIKeyCrud (139-236)
backend/app/tests/utils/auth.py (2)
  • AuthContext (9-21)
  • get_auth_context (24-83)
backend/app/api/deps.py (3)
backend/app/models/auth.py (1)
  • TokenPayload (11-12)
backend/app/models/user.py (2)
  • User (48-56)
  • UserContext (68-71)
backend/app/crud/api_key.py (1)
  • verify (92-133)
backend/app/tests/api/routes/documents/test_route_document_upload.py (2)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (1)
  • user_api_key (93-95)
backend/app/tests/conftest.py (2)
backend/app/tests/utils/utils.py (1)
  • get_superuser_token_headers (32-41)
backend/app/tests/utils/auth.py (3)
  • get_superuser_auth_context (86-110)
  • get_user_auth_context (113-137)
  • AuthContext (9-21)
backend/app/tests/utils/utils.py (2)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/assistants.py (1)
  • Assistant (33-44)
backend/app/api/routes/api_keys.py (5)
backend/app/crud/api_key.py (4)
  • APIKeyCrud (139-236)
  • create (178-217)
  • read_all (161-176)
  • delete (219-236)
backend/app/models/api_key.py (2)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/models/message.py (1)
  • Message (5-6)
backend/app/utils.py (2)
  • APIResponse (29-53)
  • success_response (36-39)
backend/app/api/permissions.py (2)
  • Permission (10-15)
  • require_permission (45-70)
backend/app/models/__init__.py (2)
backend/app/models/api_key.py (4)
  • APIKey (34-45)
  • APIKeyBase (11-18)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/models/user.py (1)
  • UserContext (68-71)
backend/app/alembic/migrate_api_key.py (1)
backend/app/core/security.py (1)
  • decrypt_api_key (125-141)
backend/app/tests/utils/document.py (2)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (1)
  • user_api_key (93-95)
backend/app/tests/api/routes/test_creds.py (2)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (3)
  • user_api_key (93-95)
  • client (56-59)
  • db (28-45)
backend/app/api/permissions.py (1)
backend/app/models/user.py (1)
  • UserContext (68-71)
backend/app/tests/utils/auth.py (4)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/tests/api/routes/test_assistants.py (4)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (3)
  • user_api_key (93-95)
  • db (28-45)
  • client (56-59)
backend/app/tests/utils/utils.py (1)
  • get_assistant (76-99)
backend/app/tests/utils/document.py (2)
  • get (118-122)
  • delete (124-128)
backend/app/tests/api/routes/collections/test_create_collections.py (3)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (2)
  • user_api_key (93-95)
  • db (28-45)
backend/app/crud/collection.py (1)
  • read_one (84-93)
backend/app/tests/core/doctransformer/test_service/conftest.py (3)
backend/app/tests/utils/auth.py (2)
  • get_user_auth_context (113-137)
  • AuthContext (9-21)
backend/app/tests/conftest.py (2)
  • db (28-45)
  • user_api_key (93-95)
backend/app/models/user.py (1)
  • UserProjectOrg (64-65)
backend/app/tests/api/routes/test_openai_conversation.py (2)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (2)
  • db (28-45)
  • user_api_key (93-95)
backend/app/crud/api_key.py (2)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/crud/project.py (1)
  • get_project_by_id (37-38)
backend/app/tests/api/routes/test_doc_transformation_job.py (4)
backend/app/models/doc_transformation_job.py (1)
  • TransformationStatus (9-13)
backend/app/tests/utils/document.py (1)
  • DocumentStore (53-80)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (4)
  • client (56-59)
  • db (28-45)
  • user_api_key (93-95)
  • superuser_api_key (87-89)
backend/app/tests/api/routes/collections/test_collection_info.py (3)
backend/app/tests/utils/auth.py (1)
  • AuthContext (9-21)
backend/app/tests/conftest.py (3)
  • db (28-45)
  • user_api_key (93-95)
  • client (56-59)
backend/app/models/collection.py (2)
  • CollectionStatus (15-18)
  • Collection (21-54)
backend/app/seed_data/seed_data.py (2)
backend/app/core/security.py (2)
  • get_password_hash (93-103)
  • encrypt_credentials (144-161)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/crud/onboarding.py (2)
backend/app/crud/api_key.py (1)
  • generate (34-56)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
🪛 GitHub Actions: AI Platform CI
backend/app/tests/utils/auth.py

[error] 1-1: Black formatting check failed. 1 file reformatted, 212 files left unchanged. Reformatted backend/app/tests/utils/auth.py. Command: 'pre-commit run --all-files'. Hook: black.

🪛 Ruff (0.13.3)
backend/app/tests/utils/utils.py

85-85: Avoid equality comparisons to False; use not Assistant.is_deleted: for false checks

Replace with not Assistant.is_deleted

(E712)

backend/app/tests/api/routes/test_creds.py

123-123: Unused function argument: db

(ARG001)


171-171: Unused function argument: db

(ARG001)


233-233: Unused function argument: db

(ARG001)


287-287: Unused function argument: db

(ARG001)


342-342: Unused function argument: db

(ARG001)

backend/app/api/permissions.py

21-21: Unused function argument: session

(ARG001)

backend/app/tests/utils/auth.py

67-67: Avoid equality comparisons to False; use not APIKey.is_deleted: for false checks

Replace with not APIKey.is_deleted

(E712)

backend/app/tests/core/doctransformer/test_service/conftest.py

55-55: Unused function argument: db

(ARG001)

backend/app/crud/api_key.py

4-4: typing.Tuple is deprecated, use tuple instead

(UP035)

🔇 Additional comments (10)
backend/app/crud/onboarding.py (1)

93-101: LGTM! API key generation refactored correctly.

The transition to api_key_manager.generate() with separate key_prefix and key_hash storage aligns with the hash-based authentication approach. The raw key is properly returned for the onboarding response.

backend/app/models/api_key.py (2)

22-31: LGTM! Public model structure improved.

The new APIKeyPublic and APIKeyCreateResponse models correctly expose the necessary fields. The separation between the persistent model and the one-time response containing the raw key is a good security practice.


34-45: LGTM! APIKey model refactored correctly.

The model properly implements the hash-based storage pattern with UUID primary key, key_prefix for lookup, and key_hash for verification. The field constraints (unique, indexed, nullable) are appropriate.

backend/app/seed_data/seed_data.json (2)

51-51: Verify API key format matches the new pattern.

Similar to Line 43, ensure this seed API key format aligns with the APIKeyManager.generate() output.


43-43: Seed API key format is correct and matches the APIKeyManager.generate() pattern.

backend/app/crud/__init__.py (1)

30-30: LGTM! API surface consolidated correctly.

The shift from individual functions to APIKeyCrud and api_key_manager provides a cleaner, more maintainable API surface. This aligns well with the PR's refactoring objectives.

backend/app/tests/api/routes/documents/test_route_document_upload.py (1)

80-81: LGTM! Test fixture updated for AuthContext pattern.

The migration from APIKeyPublic to AuthContext aligns with the broader test infrastructure refactor. The AuthContext provides richer context (user, project, organization) which improves test clarity.

backend/app/models/user.py (1)

68-72: LGTM! UserContext model added correctly.

The new UserContext class provides a clean structure for passing user identity with project and organization context through the authentication/authorization flow. The optional fields are appropriate for handling different authentication scenarios.

backend/app/tests/utils/test_data.py (2)

23-23: LGTM! Test utilities updated for new API key management.

The import of APIKeyCrud aligns with the consolidated API key CRUD operations. This is consistent with the broader refactoring effort.


33-33: LGTM! AuthContext integration added correctly.

The imports of AuthContext and get_auth_context support the new test pattern where authentication context is derived from seed data rather than creating API keys on-the-fly. This improves test consistency and reduces setup complexity.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/app/api/routes/api_keys.py (1)

31-33: Fix: use user_id from the user context.

current_user is now a UserContext, which exposes user_id (see AuthContext in backend/app/tests/utils/auth.py line 12). Calling .id raises AttributeError, breaking the POST /api-keys endpoint at runtime.

Apply this diff:

     raw_key, api_key = api_key_crud.create(
-        user_id=current_user.id,
+        user_id=current_user.user_id,
     )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86bf44b and 9915c69.

📒 Files selected for processing (2)
  • backend/app/api/routes/api_keys.py (1 hunks)
  • backend/app/tests/utils/auth.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/api/routes/api_keys.py (5)
backend/app/crud/api_key.py (4)
  • APIKeyCrud (139-236)
  • create (178-217)
  • read_all (161-176)
  • delete (219-236)
backend/app/models/api_key.py (2)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/models/message.py (1)
  • Message (5-6)
backend/app/utils.py (2)
  • APIResponse (29-53)
  • success_response (36-39)
backend/app/api/permissions.py (2)
  • Permission (10-15)
  • require_permission (45-70)
backend/app/tests/utils/auth.py (4)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
🪛 Ruff (0.13.3)
backend/app/tests/utils/auth.py

74-74: Avoid equality comparisons to False; use not APIKey.is_deleted: for false checks

Replace with not APIKey.is_deleted

(E712)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (7)
backend/app/api/routes/api_keys.py (3)

13-37: LGTM on the create endpoint structure!

The endpoint correctly returns APIKeyCreateResponse with the raw key only at creation time, uses proper permission checks via Permission.SUPERUSER, and follows the new APIKeyCrud-based flow. Once the user_id attribute issue is fixed, this implementation is solid.


40-60: LGTM on the list endpoint!

The endpoint correctly uses current_user.project_id to scope the listing, implements pagination with sensible defaults and constraints, and returns masked keys (APIKeyPublic) for security. The permission check via Permission.REQUIRE_PROJECT is appropriate.


63-79: LGTM on the delete endpoint!

The endpoint correctly uses current_user.project_id to scope the deletion, takes UUID as the key identifier, and returns a clear success message. The permission check via Permission.REQUIRE_PROJECT is appropriate.

backend/app/tests/utils/auth.py (4)

9-22: LGTM on AuthContext design!

The AuthContext class provides a clean, comprehensive test authentication context with all necessary IDs and nested objects. This design allows tests to access both scalar identifiers and full ORM objects as needed.


25-92: LGTM on get_auth_context!

The helper function correctly retrieves seeded data from the database and provides clear error messages when data is missing. The logic flow (user → project → organization → api_key) is appropriate, and all required validations are in place.


74-74: Keep the equality comparison for SQLModel query building.

The static analysis tool suggests using not APIKey.is_deleted instead of APIKey.is_deleted == False. However, this is a false positive: SQLModel/SQLAlchemy uses comparison operators to build SQL expressions, and not would produce incorrect behavior. The current code is correct.


95-119: LGTM on get_superuser_auth_context!

The helper correctly retrieves the superuser authentication context using seeded data references from settings and provides clear documentation about the expected seed data structure.

@codecov
Copy link

codecov bot commented Oct 6, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
backend/app/api/permissions.py (1)

34-40: Restore superuser bypass for all permission checks.

Superusers authenticating via JWT still fail REQUIRE_ORGANIZATION/REQUIRE_PROJECT because we only match on those branches. Please short-circuit on auth_context.user.is_superuser before the match so admins retain full access, as previously requested.

-    match permission:
+    if auth_context.user.is_superuser:
+        return True
+
+    match permission:
         case Permission.SUPERUSER:
             return auth_context.user.is_superuser
backend/app/api/routes/api_keys.py (1)

31-33: Critical: Fix attribute access on current_user.

current_user is an AuthContext object that exposes user_id, not id. This code will raise AttributeError at runtime when creating an API key.

Apply this diff:

     raw_key, api_key = api_key_crud.create(
-        user_id=current_user.id,
+        user_id=current_user.user_id,
     )
backend/app/crud/api_key.py (1)

9-9: Critical: Fix circular import.

Importing get_project_by_id from app.crud causes a circular dependency because app.crud.__init__ re-exports APIKeyCrud from this module. This will raise ImportError when the package initializes.

Apply this diff:

-from app.crud import get_project_by_id
+from app.crud.project import get_project_by_id
🧹 Nitpick comments (2)
backend/app/core/security.py (1)

15-15: Use built-in tuple instead of typing.Tuple.

Passlib/SQLModel stack has moved to favor built-in generics; Ruff is flagging typing.Tuple as deprecated (UP035). Switching the return annotation to tuple[str, str, str] keeps the linter quiet without behavior changes.

-from typing import Any, Tuple
+from typing import Any
...
-    def generate(cls) -> Tuple[str, str, str]:
+    def generate(cls) -> tuple[str, str, str]:

Also applies to: 208-231

backend/app/crud/api_key.py (1)

3-3: Modernize type hint.

Use tuple instead of typing.Tuple for Python 3.9+ compatibility.

Apply this diff:

-from typing import Tuple

Then update line 55:

-    def create(self, user_id: int) -> Tuple[str, APIKey]:
+    def create(self, user_id: int) -> tuple[str, APIKey]:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9915c69 and 7fc2584.

📒 Files selected for processing (8)
  • backend/app/api/deps.py (5 hunks)
  • backend/app/api/permissions.py (1 hunks)
  • backend/app/api/routes/api_keys.py (1 hunks)
  • backend/app/core/security.py (2 hunks)
  • backend/app/crud/api_key.py (1 hunks)
  • backend/app/models/__init__.py (1 hunks)
  • backend/app/models/auth.py (2 hunks)
  • backend/app/models/user.py (0 hunks)
💤 Files with no reviewable changes (1)
  • backend/app/models/user.py
🧰 Additional context used
🧬 Code graph analysis (7)
backend/app/models/auth.py (3)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/api/deps.py (3)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-25)
  • TokenPayload (14-15)
backend/app/core/security.py (1)
  • verify (266-327)
backend/app/models/__init__.py (2)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/models/api_key.py (4)
  • APIKey (34-45)
  • APIKeyBase (11-18)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/core/security.py (5)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/api/routes/api_keys.py (5)
backend/app/crud/api_key.py (4)
  • APIKeyCrud (16-113)
  • create (55-94)
  • read_all (38-53)
  • delete (96-113)
backend/app/models/api_key.py (2)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/models/message.py (1)
  • Message (5-6)
backend/app/utils.py (2)
  • APIResponse (29-53)
  • success_response (36-39)
backend/app/api/permissions.py (2)
  • Permission (10-15)
  • require_permission (45-70)
backend/app/api/permissions.py (1)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/crud/api_key.py (3)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/crud/project.py (1)
  • get_project_by_id (37-38)
backend/app/core/security.py (1)
  • generate (208-230)
🪛 Ruff (0.13.3)
backend/app/core/security.py

15-15: typing.Tuple is deprecated, use tuple instead

(UP035)

backend/app/api/permissions.py

21-21: Unused function argument: session

(ARG001)

backend/app/crud/api_key.py

3-3: typing.Tuple is deprecated, use tuple instead

(UP035)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (1)
backend/app/api/routes/api_keys.py (1)

57-57: AuthContext defines project_id: Confirmed project_id exists on AuthContext (backend/app/models/auth.py:20).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/app/tests/api/routes/test_assistants.py (2)

218-228: Update to TestAuthContext for consistency.

This test still uses the old user_api_key_header: dict parameter while all other tests in this file have been updated to use user_api_key: TestAuthContext. This inconsistency creates maintenance burden and confusion.

Apply this diff to align with the rest of the file:

 def test_get_assistant_not_found(
     client: TestClient,
-    user_api_key_header: dict,
+    user_api_key: TestAuthContext,
 ):
     """Test failure when fetching a non-existent assistant."""
     non_existent_id = str(uuid4())

     response = client.get(
         f"/api/v1/assistant/{non_existent_id}",
-        headers=user_api_key_header,
+        headers={"X-API-KEY": user_api_key.key},
     )

259-283: Update remaining tests to TestAuthContext for consistency.

Two more tests (test_list_assistants_invalid_pagination and test_delete_assistant_not_found) still use the old user_api_key_header: dict parameter. Update these to match the refactored pattern used throughout this file.

Apply this diff:

 def test_list_assistants_invalid_pagination(
     client: TestClient,
-    user_api_key_header: dict,
+    user_api_key: TestAuthContext,
 ):
     """Test assistants list with invalid pagination parameters."""
     # Test negative skip
     response = client.get(
         "/api/v1/assistant/?skip=-1&limit=10",
-        headers=user_api_key_header,
+        headers={"X-API-KEY": user_api_key.key},
     )
     assert response.status_code == 422

     # Test limit too high
     response = client.get(
         "/api/v1/assistant/?skip=0&limit=101",
-        headers=user_api_key_header,
+        headers={"X-API-KEY": user_api_key.key},
     )
     assert response.status_code == 422

     # Test limit too low
     response = client.get(
         "/api/v1/assistant/?skip=0&limit=0",
-        headers=user_api_key_header,
+        headers={"X-API-KEY": user_api_key.key},
     )
     assert response.status_code == 422


 def test_delete_assistant_not_found(
     client: TestClient,
-    user_api_key_header: dict,
+    user_api_key: TestAuthContext,
 ):
     """Test failure when deleting a non-existent assistant."""
     non_existent_id = str(uuid4())

     response = client.delete(
         f"/api/v1/assistant/{non_existent_id}",
-        headers=user_api_key_header,
+        headers={"X-API-KEY": user_api_key.key},
     )

Also applies to: 305-315

♻️ Duplicate comments (1)
backend/app/tests/utils/auth.py (1)

140-146: Fix the raw_key mismatch between code and docstring.

Line 144 has raw_key="ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j" (46 chars after prefix) but the docstring at line 129 documents "ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j9" (47 chars after prefix). The key is missing the final "9".

Apply this diff to match the documented seed data:

     return get_auth_context(
         session=session,
         user_email=settings.EMAIL_TEST_USER,
         project_name="Dalgo",
-        raw_key="ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j",
+        raw_key="ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j9",
         user_type="User",
     )
🧹 Nitpick comments (3)
backend/app/tests/core/doctransformer/test_service/conftest.py (1)

54-63: Remove unused db parameter.

The db parameter is no longer used since the fixture now accepts user_api_key: TestAuthContext which already provides all necessary context. Removing it improves clarity.

Apply this diff:

 @pytest.fixture
-def current_user(db: Session, user_api_key: TestAuthContext) -> UserProjectOrg:
+def current_user(user_api_key: TestAuthContext) -> UserProjectOrg:
     """Create a test user for testing."""
     api_key = user_api_key
     user = api_key.user
backend/app/tests/utils/auth.py (1)

70-75: Use not instead of == False for boolean comparison.

Per PEP8 and the static analysis hint, boolean comparisons should use not rather than explicit comparison to False.

Apply this diff:

     # Get API key for this user and project
     api_key = session.exec(
         select(APIKey)
         .where(APIKey.user_id == user.id)
         .where(APIKey.project_id == project.id)
-        .where(APIKey.is_deleted == False)
+        .where(not APIKey.is_deleted)
     ).first()
backend/app/tests/api/routes/test_creds.py (1)

122-124: Consider removing unused db parameters.

Several test functions declare a db: Session parameter that is no longer used. While harmless, removing these improves clarity:

  • test_read_credentials_not_found (line 123)
  • test_read_provider_credential_not_found (line 171)
  • test_update_credentials_not_found_for_provider (line 233)
  • test_delete_provider_credential_not_found (line 287)
  • test_delete_all_credentials_not_found (line 342)

Example fix for one test:

 def test_read_credentials_not_found(
-    client: TestClient, db: Session, user_api_key: TestAuthContext
+    client: TestClient, user_api_key: TestAuthContext
 ):

Also applies to: 170-172, 232-234, 286-288, 341-343

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fc2584 and 2243774.

📒 Files selected for processing (12)
  • backend/app/tests/api/routes/collections/test_collection_info.py (4 hunks)
  • backend/app/tests/api/routes/collections/test_create_collections.py (4 hunks)
  • backend/app/tests/api/routes/documents/test_route_document_upload.py (2 hunks)
  • backend/app/tests/api/routes/test_assistants.py (16 hunks)
  • backend/app/tests/api/routes/test_creds.py (17 hunks)
  • backend/app/tests/api/routes/test_doc_transformation_job.py (15 hunks)
  • backend/app/tests/api/routes/test_openai_conversation.py (14 hunks)
  • backend/app/tests/conftest.py (2 hunks)
  • backend/app/tests/core/doctransformer/test_service/conftest.py (2 hunks)
  • backend/app/tests/utils/auth.py (1 hunks)
  • backend/app/tests/utils/document.py (3 hunks)
  • backend/app/tests/utils/test_data.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • backend/app/tests/utils/test_data.py
  • backend/app/tests/conftest.py
  • backend/app/tests/api/routes/collections/test_collection_info.py
  • backend/app/tests/api/routes/test_openai_conversation.py
  • backend/app/tests/api/routes/documents/test_route_document_upload.py
  • backend/app/tests/utils/document.py
🧰 Additional context used
🧬 Code graph analysis (6)
backend/app/tests/api/routes/collections/test_create_collections.py (3)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/conftest.py (2)
  • user_api_key (93-95)
  • db (28-45)
backend/app/crud/collection.py (1)
  • read_one (84-93)
backend/app/tests/api/routes/test_doc_transformation_job.py (3)
backend/app/tests/utils/document.py (1)
  • DocumentStore (53-80)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/conftest.py (3)
  • db (28-45)
  • user_api_key (93-95)
  • superuser_api_key (87-89)
backend/app/tests/core/doctransformer/test_service/conftest.py (3)
backend/app/tests/utils/auth.py (2)
  • get_user_auth_context (122-146)
  • TestAuthContext (9-22)
backend/app/tests/conftest.py (2)
  • db (28-45)
  • user_api_key (93-95)
backend/app/models/user.py (1)
  • UserProjectOrg (64-65)
backend/app/tests/api/routes/test_assistants.py (4)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/conftest.py (3)
  • user_api_key (93-95)
  • db (28-45)
  • client (56-59)
backend/app/tests/utils/utils.py (1)
  • get_assistant (76-99)
backend/app/tests/utils/document.py (2)
  • get (118-122)
  • delete (124-128)
backend/app/tests/api/routes/test_creds.py (2)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/conftest.py (3)
  • user_api_key (93-95)
  • client (56-59)
  • db (28-45)
backend/app/tests/utils/auth.py (4)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
🪛 Ruff (0.13.3)
backend/app/tests/core/doctransformer/test_service/conftest.py

55-55: Unused function argument: db

(ARG001)

backend/app/tests/api/routes/test_creds.py

123-123: Unused function argument: db

(ARG001)


171-171: Unused function argument: db

(ARG001)


233-233: Unused function argument: db

(ARG001)


287-287: Unused function argument: db

(ARG001)


342-342: Unused function argument: db

(ARG001)

backend/app/tests/utils/auth.py

74-74: Avoid equality comparisons to False; use not APIKey.is_deleted: for false checks

Replace with not APIKey.is_deleted

(E712)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (3)
backend/app/tests/api/routes/collections/test_create_collections.py (1)

14-14: LGTM!

The migration to TestAuthContext is complete and consistent. The test properly uses user_api_key.project_id, user_api_key.user_id, and user_api_key.key throughout, aligning with the new authentication-context-based testing pattern.

Also applies to: 51-95

backend/app/tests/api/routes/test_doc_transformation_job.py (1)

6-8: LGTM!

All test methods have been consistently updated to use TestAuthContext. The refactoring is complete and correct, with proper usage of user_api_key.key, user_api_key.project_id, and superuser_api_key throughout the test suite.

Also applies to: 13-315

backend/app/tests/api/routes/test_creds.py (1)

16-16: LGTM!

The migration to TestAuthContext is consistent across all test functions. The test suite properly uses user_api_key.project_id, user_api_key.organization_id, and user_api_key.key throughout.

Also applies to: 24-521

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
backend/app/crud/api_key.py (1)

55-99: Refine exception handling to preserve HTTPException status codes.

The broad except Exception block (line 90) catches HTTPException instances raised at lines 62 and 67, then re-wraps them as 500 errors. This loses the original 404 status codes and detail messages.

Apply this diff to fix exception handling:

-        except Exception as e:
+        except HTTPException:
+            raise
+        except Exception as e:
             logger.error(
                 f"[APIKeyCrud.create_api_key] Failed to create API key | "
                 f"{{'project_id': {self.project_id}, 'user_id': {user_id}, 'error': '{str(e)}'}}",
🧹 Nitpick comments (1)
backend/app/crud/api_key.py (1)

55-55: Use tuple instead of deprecated typing.Tuple.

Python 3.9+ supports using the built-in tuple type for type hints.

Apply this diff:

-    def create(self, user_id: int, project_id: int) -> Tuple[str, APIKey]:
+    def create(self, user_id: int, project_id: int) -> tuple[str, APIKey]:

And remove the import:

-from typing import Tuple
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2243774 and 7062525.

📒 Files selected for processing (2)
  • backend/app/api/routes/api_keys.py (1 hunks)
  • backend/app/crud/api_key.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/crud/api_key.py (4)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/crud/project.py (1)
  • get_project_by_id (37-38)
backend/app/core/security.py (1)
  • generate (208-230)
backend/app/api/routes/api_keys.py (5)
backend/app/crud/api_key.py (4)
  • APIKeyCrud (16-118)
  • create (55-99)
  • read_all (38-53)
  • delete (101-118)
backend/app/models/api_key.py (2)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/models/message.py (1)
  • Message (5-6)
backend/app/utils.py (2)
  • APIResponse (29-53)
  • success_response (36-39)
backend/app/api/permissions.py (2)
  • Permission (10-15)
  • require_permission (45-70)
🪛 Ruff (0.13.3)
backend/app/crud/api_key.py

3-3: typing.Tuple is deprecated, use tuple instead

(UP035)

backend/app/api/routes/api_keys.py

22-22: Unused function argument: current_user

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (4)
backend/app/api/routes/api_keys.py (2)

47-62: LGTM!

The list route correctly uses current_user.project_id for project-scoped access and includes proper pagination support.


70-81: LGTM!

The delete route correctly uses current_user.project_id to ensure project-scoped deletion and returns an appropriate success message.

backend/app/crud/api_key.py (2)

38-53: LGTM!

The read_all method correctly implements project-scoped filtering with pagination support.


101-118: LGTM!

The delete method correctly implements soft deletion with project-scoped access control via read_one.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
backend/app/tests/utils/auth.py (1)

144-144: Fix the raw_key to match the documented seed data.

Line 144 has the key ending with "...ai8j" but the docstring at line 129 documents it as "...ai8j9" (with a final "9"). This mismatch will cause authentication failures.

Apply this diff:

-        raw_key="ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j",
+        raw_key="ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j9",
backend/app/crud/api_key.py (2)

27-27: Fix the docstring to match implementation.

The docstring states "by its key_prefix" but the method queries by key_id (UUID) on line 31.

Apply this diff:

-        Retrieve a single non-deleted API key by its key_prefix.
+        Retrieve a single non-deleted API key by its ID.

90-99: Refine exception handling to avoid wrapping HTTPException.

The broad except Exception will catch and re-wrap any HTTPException raised by get_project_by_id or user validation (lines 59-66) as a 500 error, losing the original status code.

Apply this diff:

+        except HTTPException:
+            raise
         except Exception as e:
             logger.error(
🧹 Nitpick comments (2)
backend/app/tests/utils/auth.py (1)

74-74: Use idiomatic comparison for boolean check.

The static analysis hint is valid: prefer APIKey.is_deleted.is_(False) over == False for SQLModel/SQLAlchemy boolean comparisons to ensure proper SQL generation.

Apply this diff:

-        .where(APIKey.is_deleted == False)
+        .where(APIKey.is_deleted.is_(False))
backend/app/crud/api_key.py (1)

3-3: Use modern type annotation.

The static analysis hint is valid: typing.Tuple is deprecated in favor of the built-in tuple type (Python 3.9+).

Apply this diff:

-from typing import Tuple
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from collections.abc import Tuple
+else:
+    Tuple = tuple

Or simply:

-from typing import Tuple

And use tuple[str, APIKey] at line 55.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7062525 and 7755b3b.

📒 Files selected for processing (9)
  • backend/app/api/deps.py (5 hunks)
  • backend/app/crud/api_key.py (1 hunks)
  • backend/app/tests/api/routes/test_api_key.py (1 hunks)
  • backend/app/tests/conftest.py (2 hunks)
  • backend/app/tests/core/doctransformer/test_service/conftest.py (2 hunks)
  • backend/app/tests/core/test_security.py (2 hunks)
  • backend/app/tests/crud/test_api_key.py (1 hunks)
  • backend/app/tests/utils/auth.py (1 hunks)
  • backend/app/tests/utils/test_data.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/tests/utils/test_data.py
🧰 Additional context used
🧬 Code graph analysis (8)
backend/app/tests/core/test_security.py (5)
backend/app/core/security.py (4)
  • APIKeyManager (191-327)
  • generate (208-230)
  • _extract_key_parts (233-263)
  • verify (266-327)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/tests/utils/test_data.py (1)
  • create_test_api_key (87-107)
backend/app/tests/conftest.py (1)
  • db (28-45)
backend/app/tests/api/routes/test_api_key.py (5)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/utils/test_data.py (2)
  • create_test_api_key (87-107)
  • create_test_project (47-63)
backend/app/tests/conftest.py (5)
  • db (28-45)
  • client (56-59)
  • superuser_token_headers (63-64)
  • normal_user_token_headers (68-71)
  • user_api_key (93-95)
backend/app/tests/crud/test_api_key.py (1)
  • test_delete_api_key (208-222)
backend/app/crud/api_key.py (1)
  • delete (101-118)
backend/app/crud/api_key.py (4)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/crud/project.py (1)
  • get_project_by_id (37-38)
backend/app/core/security.py (1)
  • generate (208-230)
backend/app/api/deps.py (7)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-25)
  • TokenPayload (14-15)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/project_user.py (1)
  • ProjectUser (27-35)
backend/app/models/user.py (3)
  • User (48-56)
  • UserOrganization (59-61)
  • UserProjectOrg (64-65)
backend/app/core/security.py (1)
  • verify (266-327)
backend/app/tests/conftest.py (2)
backend/app/tests/utils/utils.py (1)
  • get_superuser_token_headers (32-41)
backend/app/tests/utils/auth.py (3)
  • get_superuser_test_auth_context (95-119)
  • get_user_test_auth_context (122-146)
  • TestAuthContext (9-22)
backend/app/tests/utils/auth.py (4)
backend/app/models/user.py (1)
  • User (48-56)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/tests/crud/test_api_key.py (4)
backend/app/crud/api_key.py (5)
  • APIKeyCrud (16-118)
  • create (55-99)
  • read_one (25-36)
  • delete (101-118)
  • read_all (38-53)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/tests/utils/test_data.py (2)
  • create_test_project (47-63)
  • create_test_api_key (87-107)
backend/app/tests/utils/utils.py (1)
  • get_non_existent_id (49-51)
backend/app/tests/core/doctransformer/test_service/conftest.py (3)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/conftest.py (2)
  • db (28-45)
  • user_api_key (93-95)
backend/app/models/user.py (1)
  • UserProjectOrg (64-65)
🪛 Ruff (0.13.3)
backend/app/crud/api_key.py

3-3: typing.Tuple is deprecated, use tuple instead

(UP035)

backend/app/tests/utils/auth.py

74-74: Avoid equality comparisons to False; use not APIKey.is_deleted: for false checks

Replace with not APIKey.is_deleted

(E712)

backend/app/tests/core/doctransformer/test_service/conftest.py

55-55: Unused function argument: db

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (24)
backend/app/api/deps.py (4)

51-53: LGTM!

The API key verification logic correctly uses the new api_key_manager.verify and handles the None case appropriately.


97-100: LGTM!

The API key verification and organization validation are correct. The function properly handles the optional API key scenario.


118-122: LGTM!

The API key verification and validation logic is correct for the project-organization context.


153-208: LGTM!

The new get_auth_context function provides a clean separation between authentication and authorization. The error handling is comprehensive, covering invalid credentials, inactive entities, and missing authentication.

backend/app/tests/crud/test_api_key.py (5)

13-30: LGTM!

The test properly validates all fields of the created API key, including the new key_prefix, key_hash, and is_deleted fields.


33-58: LGTM!

Good coverage of error cases for nonexistent project and user. The tests correctly verify the 404 status codes and error messages.


61-106: LGTM!

Comprehensive coverage of read_one scenarios: successful read, nonexistent key, wrong project scope, and deleted keys. All edge cases are handled correctly.


109-205: LGTM!

Excellent test coverage for read_all: basic listing, pagination, exclusion of deleted keys, and project scoping. The tests validate that project isolation works correctly.


208-267: LGTM!

Thorough testing of delete operations covering successful deletion, nonexistent keys, wrong project scope, and double deletion attempts. All error paths are validated.

backend/app/tests/core/doctransformer/test_service/conftest.py (1)

55-63: LGTM!

The fixture correctly uses TestAuthContext to derive the user and organization/project IDs. The static analysis hint about unused db is a false positive—the parameter is required for pytest's dependency injection even though it's not directly used in the function body.

backend/app/tests/core/test_security.py (1)

129-308: LGTM!

Excellent comprehensive test coverage for APIKeyManager:

  • Key generation (format, length, uniqueness, bcrypt hash)
  • Key parsing (new/old formats, invalid inputs)
  • Verification (valid keys, invalid keys, wrong secrets, deleted keys, malformed inputs)
  • Constants validation
  • Integration test confirming generated keys can be verified

The test suite validates all critical paths and edge cases.

backend/app/tests/conftest.py (1)

74-95: LGTM!

The fixture updates correctly use the new TestAuthContext pattern, simplifying authentication setup by leveraging seeded data instead of creating new API keys per test.

backend/app/tests/api/routes/test_api_key.py (4)

12-40: LGTM!

The test correctly validates the new response structure with expanded fields (id, key_prefix, organization_id) and confirms the key format starts with "ApiKey ".


43-57: LGTM!

Good authorization test confirming normal users receive 403 when attempting to create API keys (superuser-only operation).


60-84: LGTM!

The list test creates multiple keys and validates the response includes at least the expected count. The >= 4 assertion accounts for both created keys and the fixture key.


87-114: LGTM!

Good coverage of delete operations: successful deletion with message validation and 404 for nonexistent keys.

backend/app/tests/utils/auth.py (3)

9-22: LGTM!

The TestAuthContext model provides a clean structure for test authentication with all necessary IDs and nested objects.


25-92: LGTM!

The helper function properly assembles authentication context from seeded data with comprehensive error handling for missing entities.


95-119: LGTM!

The superuser context helper correctly references the seeded data for Glific project and documents the expected values.

backend/app/crud/api_key.py (5)

16-23: LGTM!

The class initialization properly stores the session and project_id for scoped operations.


38-53: LGTM!

The read_all method correctly filters by project_id, excludes deleted keys, and supports pagination.


59-67: LGTM!

The project and user validation with appropriate 404 errors is correct (per past review).


68-88: LGTM!

The API key generation and persistence logic correctly uses the new api_key_manager and stores key_prefix and key_hash.


101-118: LGTM!

The soft delete implementation correctly marks the key as deleted, updates the timestamp, and raises 404 for missing keys.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/app/api/deps.py (3)

50-61: Fix misleading variable name and eliminate redundant database query.

The variable api_key_record holds an AuthContext object (which includes a User object), not an APIKey record. Additionally, line 55 performs a redundant database query since api_key_manager.verify() already eagerly loads the User object.

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if not api_key_record:
+        auth_context = api_key_manager.verify(session, api_key)
+        if not auth_context:
             raise HTTPException(status_code=401, detail="Invalid API Key")
 
-        user = session.get(User, api_key_record.user_id)
-        if not user:
-            raise HTTPException(
-                status_code=404, detail="User linked to API Key not found"
-            )
-
-        return user  # Return only User object
+        return auth_context.user  # Return only User object

50-61: Add inactive entity checks for API key authentication.

The API key authentication path doesn't validate that the user, organization, and project are active, while the token path does check for inactive users. This inconsistency could allow inactive entities to authenticate via API key. The new get_auth_context function properly validates all inactive states—consider using similar checks here or migrating to get_auth_context.

Apply this diff to add the missing checks:

     if api_key:
         auth_context = api_key_manager.verify(session, api_key)
         if not auth_context:
             raise HTTPException(status_code=401, detail="Invalid API Key")
 
+        if not auth_context.user.is_active:
+            raise HTTPException(status_code=403, detail="Inactive user")
+        
+        if auth_context.organization and not auth_context.organization.is_active:
+            raise HTTPException(status_code=403, detail="Inactive Organization")
+        
+        if auth_context.project and not auth_context.project.is_active:
+            raise HTTPException(status_code=403, detail="Inactive Project")
+
         return auth_context.user  # Return only User object

117-122: Remove redundant organization validation.

Similar to get_current_user_org, the api_key_manager.verify() method already validates and loads the organization, making the validate_organization() call redundant.

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization_id)
-            organization_id = api_key_record.organization_id
-            project_id = api_key_record.project_id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            if not auth_context.organization.is_active:
+                raise HTTPException(status_code=400, detail="Organization is not active")
+            organization_id = auth_context.organization_id
+            project_id = auth_context.project_id
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7755b3b and 5425690.

📒 Files selected for processing (3)
  • backend/app/api/deps.py (5 hunks)
  • backend/app/tests/api/test_deps.py (2 hunks)
  • backend/app/tests/utils/collection.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
backend/app/api/deps.py (3)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-25)
  • TokenPayload (14-15)
backend/app/core/security.py (1)
  • verify (266-327)
backend/app/tests/utils/collection.py (2)
backend/app/tests/utils/test_data.py (1)
  • create_test_api_key (87-107)
backend/app/tests/conftest.py (1)
  • db (28-45)
backend/app/tests/api/test_deps.py (4)
backend/app/api/deps.py (1)
  • get_auth_context (153-204)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/utils/test_data.py (2)
  • create_test_api_key (87-107)
  • create_test_project (47-63)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (4)
backend/app/tests/utils/collection.py (2)

11-11: LGTM! Import correctly migrated to test utilities.

The switch from create_api_key (CRUD) to create_test_api_key (test helper) appropriately aligns with the PR's migration to hash-based keys where raw keys can't be retrieved post-creation.


32-32: Verify that the unused return value is intentional.

The migration to create_test_api_key is correct. However, the function returns an APIKeyCreateResponse (containing the raw key and API key details) that is not captured or used.

Given that line 35 uses a hardcoded "test_api_key" for the OpenAI client and the Collection object references project.organization_id directly, this appears to be intentional—the API key creation is solely for establishing database relationships/foreign keys.

Please confirm this is the intended behavior and the raw key isn't needed elsewhere in the test flow.

backend/app/tests/api/test_deps.py (1)

169-321: LGTM! Comprehensive test coverage for authentication.

The test suite thoroughly validates all authentication paths including valid/invalid API keys, valid/invalid tokens, missing credentials, and inactive entity handling. The tests properly verify both success and error scenarios with appropriate status codes and error messages.

backend/app/api/deps.py (1)

153-204: LGTM! Well-structured authentication context function.

The new get_auth_context function properly centralizes authentication logic with comprehensive validation for both API key and JWT token flows. It correctly handles:

  • API key path: validates and returns full context (user, project, organization)
  • Token path: validates and returns user-only context
  • Proper inactive entity checks for all paths
  • Appropriate HTTP status codes (401 for auth failures, 403 for inactive entities)

The separation of authentication concerns into this dedicated function improves code organization and maintainability.

…KeyManager class, add delete API key route, and update related CRUD operations
…anager, update key prefix extraction logic, and adjust seed data creation for API keys.
- Removed direct usage of APIKeyPublic in tests and replaced with AuthContext for better encapsulation of authentication details.
- Updated test cases across various modules (e.g., test_api_key, test_assistants, test_creds, etc.) to utilize the new AuthContext structure.
- Simplified the retrieval of API keys and user context in test setups, enhancing readability and maintainability.
- Removed redundant API key creation logic from test utilities and centralized it within the AuthContext.
- Ensured all tests still pass with the new structure, maintaining functionality while improving code organization.
…erification logic for API key authentication
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 786992e and 45df0c3.

📒 Files selected for processing (4)
  • backend/app/crud/__init__.py (1 hunks)
  • backend/app/models/__init__.py (2 hunks)
  • backend/app/models/user.py (0 hunks)
  • backend/app/tests/utils/collection.py (1 hunks)
💤 Files with no reviewable changes (1)
  • backend/app/models/user.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/tests/utils/collection.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/crud/__init__.py
  • backend/app/models/__init__.py
backend/app/crud/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Implement database access operations in backend/app/crud/

Files:

  • backend/app/crud/__init__.py
backend/app/models/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Define SQLModel entities (database tables and domain objects) in backend/app/models/

Files:

  • backend/app/models/__init__.py
🧬 Code graph analysis (2)
backend/app/crud/__init__.py (1)
backend/app/crud/api_key.py (1)
  • APIKeyCrud (16-118)
backend/app/models/__init__.py (2)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/models/api_key.py (3)
  • APIKey (34-45)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
🪛 Ruff (0.13.3)
backend/app/models/__init__.py

113-113: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (3)
backend/app/models/__init__.py (2)

3-3: LGTM!

The addition of AuthContext to the public exports aligns with the PR's introduction of AuthContext-based authentication flows.


5-5: LGTM!

The addition of APIKeyCreateResponse supports the new API key creation flow where the raw key is exposed only once at creation, aligning with the hash-based storage approach.

backend/app/crud/__init__.py (1)

29-29: LGTM!

The consolidation from individual API key functions to the class-based APIKeyCrud and centralized api_key_manager aligns with the PR's objective to introduce an APIKeyManager class for centralized authentication logic.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py (2)

22-64: Add return annotations to comply with project typing rules

Per the project guideline to use type hints in Python files, please add explicit -> None annotations to both upgrade and downgrade.

As per coding guidelines
Apply this diff:

-def upgrade():
+def upgrade() -> None:
@@
-def downgrade():
-    pass
+def downgrade() -> None:
+    pass

38-45: Close the Alembic session via context manager

Wrap the migration session in a with Session(bind=bind) block so the connection is deterministically closed/rolled back on failure before the subsequent DDL runs.

Apply this diff:

-    bind = op.get_bind()
-    session = Session(bind=bind)
-    migrate_api_keys(session, generate_uuid=True)
-
-    # Step 4: Verify migration was successful
-    if not verify_migration(session):
-        raise Exception("API key migration verification failed. Please check the logs.")
+    bind = op.get_bind()
+    with Session(bind=bind) as session:
+        migrate_api_keys(session, generate_uuid=True)
+
+        # Step 4: Verify migration was successful
+        if not verify_migration(session):
+            raise Exception(
+                "API key migration verification failed. Please check the logs."
+            )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45df0c3 and 1fa1c32.

📒 Files selected for processing (2)
  • backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py (1 hunks)
  • backend/app/models/__init__.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/models/init.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py
🧬 Code graph analysis (1)
backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py (1)
backend/app/alembic/migrate_api_key.py (2)
  • migrate_api_keys (34-154)
  • verify_migration (157-198)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (1)
backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py (1)

51-55: Confirm and handle foreign key dependencies before replacing the primary key
Automated searches found no ForeignKey('apikey.id') in models or op.create_foreign_key in migrations; please manually audit for any remaining constraints (e.g. raw SQL, views, external services) and update or remove them in this migration to avoid orphaned references.

Copy link
Collaborator

@kartpop kartpop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP PR review - some initial comments

# ### end Alembic commands ###


def downgrade():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be something over here which does the reverse of migrate_api_keys?

what would be the repurcussions if we had to rollback and did nothing on downgrade?

Copy link
Collaborator Author

@avirajsingh7 avirajsingh7 Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of migration we will use db snapshot for downgrade.
Downgrade through script not possible due to hashing.
Added a comment.

Comment on lines 19 to 25
user_id: int
project_id: int | None = None
organization_id: int | None = None

user: User
organization: Organization | None = None
project: Project | None = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to have redundant fields here?

if user.id == user_id, why have both user and user_id fields?

similar questions for project and organization

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we’re already fetching the user, organization and project when validating the API key, we could just pass them along to the route instead of fetching again.
Reduce redundant lookup.

Well not sure how much this optimize,
What's your opinion we can remove this also.

Comment on lines +28 to +29
The raw API key is returned only once during creation.
Store it securely as it cannot be retrieved again.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raw API key is returned only once during creation.
Store it securely as it cannot be retrieved again.

i guess this message should also be sent in the metadata of the response; this is important info that should be known to the consumer of this endpoint

right now it is only superuser who can create this, so might be less relevant in the current scheme of things, but given that we already have a metadata field in our response payload, including this should be straightforward

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a message.



def downgrade():
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the downgrade here empty

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refer here

from app.api.deps import AuthContextDep, SessionDep


class Permission(str, Enum):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this permisson being set for the check

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each routes set a permission.
Check api_key routes.

created_at: Optional[str] = None


class CredentialData(BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possible to confuse this with our byok credential module, maybe should be named AuthData

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats creds data only.

project_id: int | None = None
organization_id: int | None = None

user: User
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are these fields exactly

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is User Project Organization object.
more context

response_model=APIResponse[Message],
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def delete_api_key_route(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understandable that redundant logging would not be needed but there is no info log in crud delete as well as delete router, i feel like there should be one at least at one place, tracebility should be there for happy instances as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is already a log in delete method in crud.

- Updated downgrade function to clarify snapshot restoration.
- Changed conditional logic in get_current_user function for clarity.
- Enhanced create_api_key_route response with security message about raw API key.
- Updated APIKeyManager docstring
- removed unnecessary comments
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/api/deps.py (1)

117-122: Remove redundant organization validation (duplicate issue).

Similar to get_current_user_org, this function performs redundant validation since api_key_manager.verify() already loads and validates the organization.

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization_id)
-            organization_id = api_key_record.organization_id
-            project_id = api_key_record.project_id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            if not auth_context.organization.is_active:
+                raise HTTPException(status_code=400, detail="Organization is not active")
+            organization_id = auth_context.organization_id
+            project_id = auth_context.project_id
♻️ Duplicate comments (2)
backend/app/core/security.py (1)

15-15: Update to use modern type annotation.

The code uses the deprecated typing.Tuple instead of the built-in tuple type hint.

Apply this diff:

-from typing import Any, Tuple
+from typing import Any

Then update the return type annotations to use lowercase tuple:

  • Line 218: def generate(cls) -> tuple[str, str, str]:
  • Line 243: def _extract_key_parts(cls, raw_key: str) -> tuple[str, str] | None:
backend/app/api/deps.py (1)

96-100: Remove redundant organization validation.

The api_key_manager.verify() method already validates that the organization exists and eagerly loads it in a single query. The subsequent validate_organization() call on line 99 performs a redundant database query.

Apply this diff:

     api_key = request.headers.get("X-API-KEY")
     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization_id)
-            organization_id = api_key_record.organization_id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            if not auth_context.organization.is_active:
+                raise HTTPException(status_code=400, detail="Organization is not active")
+            organization_id = auth_context.organization_id
🧹 Nitpick comments (1)
backend/app/core/security.py (1)

226-233: Consider the implications of padding for fixed-length keys.

The code uses .ljust(length, "A") to pad tokens to exact lengths. While this ensures predictable key sizes, padding with a fixed character slightly reduces entropy if secrets.token_urlsafe() occasionally produces shorter output than expected (though rare). Since token_urlsafe(n) typically generates consistent-length output, the padding serves primarily as a defensive safeguard.

If you prefer to avoid any entropy reduction, consider regenerating until exact length is achieved:

def _generate_exact_length_token(byte_count: int, target_length: int) -> str:
    """Generate a token of exact target length."""
    while True:
        token = secrets.token_urlsafe(byte_count)
        if len(token) >= target_length:
            return token[:target_length]

However, the current padding approach is simpler and the entropy reduction is negligible for cryptographic purposes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fa1c32 and f51bbcd.

📒 Files selected for processing (5)
  • backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py (1 hunks)
  • backend/app/api/deps.py (6 hunks)
  • backend/app/api/routes/api_keys.py (1 hunks)
  • backend/app/core/security.py (2 hunks)
  • backend/app/tests/core/test_security.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/core/security.py
  • backend/app/api/deps.py
  • backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py
  • backend/app/api/routes/api_keys.py
  • backend/app/tests/core/test_security.py
backend/app/core/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Place core functionality (config, DB session, security, exceptions, middleware) in backend/app/core/

Files:

  • backend/app/core/security.py
backend/app/api/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Expose FastAPI REST endpoints under backend/app/api/ organized by domain

Files:

  • backend/app/api/deps.py
  • backend/app/api/routes/api_keys.py
🧬 Code graph analysis (5)
backend/app/core/security.py (5)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/user.py (1)
  • User (48-54)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-60)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/api/deps.py (3)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-25)
  • TokenPayload (14-15)
backend/app/core/security.py (1)
  • verify (276-337)
backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py (1)
backend/app/alembic/migrate_api_key.py (2)
  • migrate_api_keys (34-154)
  • verify_migration (157-198)
backend/app/api/routes/api_keys.py (5)
backend/app/crud/api_key.py (4)
  • APIKeyCrud (16-118)
  • create (55-98)
  • read_all (38-53)
  • delete (100-118)
backend/app/models/api_key.py (2)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/models/message.py (1)
  • Message (5-6)
backend/app/utils.py (2)
  • APIResponse (29-53)
  • success_response (36-39)
backend/app/api/permissions.py (2)
  • Permission (10-15)
  • require_permission (45-70)
backend/app/tests/core/test_security.py (5)
backend/app/core/security.py (4)
  • APIKeyManager (191-337)
  • generate (218-240)
  • _extract_key_parts (243-273)
  • verify (276-337)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/tests/utils/test_data.py (1)
  • create_test_api_key (87-107)
backend/app/tests/conftest.py (1)
  • db (28-45)
🪛 Ruff (0.14.0)
backend/app/core/security.py

15-15: typing.Tuple is deprecated, use tuple instead

(UP035)

backend/app/api/routes/api_keys.py

22-22: Unused function argument: current_user

(ARG001)

🔇 Additional comments (10)
backend/app/alembic/versions/a06c34a6d730_refactor_api_key_table.py (1)

42-60: Migration logic and verification are well-structured.

The step-by-step approach is clear:

  1. Add nullable columns for transition
  2. Migrate data with verification
  3. Enforce constraints after successful migration
  4. Update primary key and indexes

The verification step correctly raises an exception on failure, preventing partial migrations.

backend/app/core/security.py (1)

191-340: LGTM! Well-designed API key management system.

The APIKeyManager class provides a secure and well-structured approach to API key handling:

  • Uses secrets.token_urlsafe() for cryptographically secure random generation
  • Employs bcrypt hashing (one-way, not reversible)
  • Maintains backward compatibility with old key format
  • Efficiently loads related entities (User, Organization, Project) in a single query
  • Returns an AuthContext for streamlined authentication flow
  • Includes proper error handling and logging
backend/app/tests/core/test_security.py (1)

129-298: LGTM! Comprehensive test coverage for APIKeyManager.

The test suite thoroughly validates the APIKeyManager functionality:

  • Key generation correctness (format, length, uniqueness)
  • Bcrypt hash format validation
  • Key part extraction for both old (43-char) and new (65-char) formats
  • Verification scenarios (valid keys, invalid keys, wrong secrets, deleted keys, malformed keys)
  • Integration test ensuring generated keys can be verified
  • Edge cases and error conditions

The tests are well-organized, clearly documented, and use proper fixtures.

backend/app/api/deps.py (2)

50-62: LGTM! Properly structured authentication flow.

The if/elif structure correctly prioritizes API key authentication over JWT token authentication, as requested in previous reviews. The use of api_key_manager.verify() is appropriate.


153-207: LGTM! Well-structured authentication context provider.

The get_auth_context function provides a clean authentication flow:

  • Prioritizes API key over JWT token (consistent with other auth functions)
  • Validates all related entities (user, organization, project) are active for API keys
  • Returns a streamlined AuthContext for downstream use
  • Proper error handling with appropriate HTTP status codes (401 for invalid auth, 403 for inactive entities, 404 for missing user)
  • Clear separation of authentication (this function) from authorization (route-level permissions)

This aligns well with the broader refactoring to permission-based authorization.

backend/app/api/routes/api_keys.py (5)

19-35: Verify: Is accepting arbitrary user_id intentional for admin functionality?

The route accepts user_id as a parameter (line 21), allowing superusers to create API keys for any user. This was flagged in a previous review as a security concern. While the route is restricted to SUPERUSER permission (line 17), accepting arbitrary user_id is broader than necessary if the intent is for users to create their own keys.

If the intention is for superusers to create keys only for themselves, apply this diff:

 def create_api_key_route(
     project_id: int,
-    user_id: int,
     current_user: AuthContextDep,
     session: SessionDep,
 ):
     """
-    Create a new API key for the project and user, Restricted to Superuser.
+    Create a new API key for the current user on the project, Restricted to Superuser.

     The raw API key is returned only once during creation.
     Store it securely as it cannot be retrieved again.
     """
     api_key_crud = APIKeyCrud(session=session, project_id=project_id)
     raw_key, api_key = api_key_crud.create(
-        user_id=user_id,
+        user_id=current_user.user_id,
         project_id=project_id,
     )

However, if the design intent is for superusers to create API keys on behalf of other users (e.g., for administrative or onboarding purposes), the current implementation is acceptable. Please confirm whether this broader permission is intentional.


22-22: Note: current_user is not actually unused.

Static analysis flagged current_user as unused, but this is a false positive. The parameter is used indirectly through the AuthContextDep dependency, which enforces authentication and provides the necessary context for the permission check via require_permission(Permission.SUPERUSER).


37-43: LGTM! Clear communication about one-time key disclosure.

The response properly includes the raw key and a clear metadata message warning users that the key will only be shown once, addressing the concern from previous reviews.


46-66: LGTM! Well-designed list endpoint.

The list endpoint:

  • Uses project-scoped APIKeyCrud for proper isolation
  • Returns only APIKeyPublic (excludes sensitive hash data)
  • Implements pagination with sensible defaults and validation
  • Requires project-level permission
  • Clear documentation about returning masked keys

69-85: LGTM! Secure deletion endpoint.

The delete endpoint:

  • Performs project-scoped deletion (prevents cross-project access)
  • Requires project-level permission
  • Returns clear success message
  • Delegates to CRUD layer for soft-delete implementation

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/api/deps.py (1)

114-119: Remove redundant organization validation.

Similar to the previous occurrence, api_key_manager.verify() already validates the organization, making the validate_organization() call redundant.

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization_id)
-            organization_id = api_key_record.organization_id
-            project_id = api_key_record.project_id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            organization_id = auth_context.organization_id
+            project_id = auth_context.project_id
♻️ Duplicate comments (2)
backend/app/seed_data/seed_data.py (1)

196-201: Prefix length mismatch breaks authentication.

The code extracts only 12 characters for key_prefix and hashes from index 12 onward, but APIKeyManager.parse_api_key() expects a 16-character prefix. This leaks the last 4 prefix characters into the hash, causing all seeded keys to fail verification.

Apply this diff to fix the prefix length:

-        key_prefix = key_portion[:12]  # First 12 characters as prefix
+        key_prefix = key_portion[:16]  # First 16 characters as prefix (matches APIKeyManager)
 
         from passlib.context import CryptContext
 
         pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
-        key_hash = pwd_context.hash(key_portion[12:])
+        secret = key_portion[16:]
+        if not secret:
+            raise ValueError("API key secret segment is empty")
+        key_hash = pwd_context.hash(secret)
backend/app/api/deps.py (1)

93-97: Remove redundant organization validation.

api_key_manager.verify() already validates that the organization exists and is active, and eagerly loads it. The subsequent validate_organization() call performs a redundant database query.

Apply this diff:

     api_key = request.headers.get("X-API-KEY")
     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization_id)
-            organization_id = api_key_record.organization_id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            organization_id = auth_context.organization_id
🧹 Nitpick comments (1)
backend/app/api/deps.py (1)

47-58: Rename api_key_record to auth_context for clarity.

The variable api_key_record holds an AuthContext (returned by api_key_manager.verify()), not an API key record. Renaming improves code clarity and aligns with the type being returned.

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if not api_key_record:
+        auth_context = api_key_manager.verify(session, api_key)
+        if not auth_context:
             raise HTTPException(status_code=401, detail="Invalid API Key")
 
-        user = session.get(User, api_key_record.user_id)
+        user = session.get(User, auth_context.user_id)
         if not user:
             raise HTTPException(
                 status_code=404, detail="User linked to API Key not found"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f51bbcd and c19da66.

📒 Files selected for processing (10)
  • backend/app/api/deps.py (6 hunks)
  • backend/app/models/__init__.py (1 hunks)
  • backend/app/models/project.py (0 hunks)
  • backend/app/models/user.py (0 hunks)
  • backend/app/seed_data/seed_data.json (1 hunks)
  • backend/app/seed_data/seed_data.py (3 hunks)
  • backend/app/tests/api/routes/test_creds.py (16 hunks)
  • backend/app/tests/api/test_deps.py (1 hunks)
  • backend/app/tests/conftest.py (2 hunks)
  • backend/app/tests/utils/test_data.py (3 hunks)
💤 Files with no reviewable changes (2)
  • backend/app/models/project.py
  • backend/app/models/user.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/app/seed_data/seed_data.json
  • backend/app/tests/api/test_deps.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/seed_data/seed_data.py
  • backend/app/tests/conftest.py
  • backend/app/api/deps.py
  • backend/app/tests/utils/test_data.py
  • backend/app/tests/api/routes/test_creds.py
  • backend/app/models/__init__.py
backend/app/api/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Expose FastAPI REST endpoints under backend/app/api/ organized by domain

Files:

  • backend/app/api/deps.py
backend/app/models/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Define SQLModel entities (database tables and domain objects) in backend/app/models/

Files:

  • backend/app/models/__init__.py
🧬 Code graph analysis (6)
backend/app/seed_data/seed_data.py (2)
backend/app/core/security.py (2)
  • get_password_hash (100-110)
  • encrypt_credentials (151-168)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/tests/conftest.py (2)
backend/app/tests/utils/utils.py (1)
  • get_superuser_token_headers (32-41)
backend/app/tests/utils/auth.py (3)
  • get_superuser_test_auth_context (95-119)
  • get_user_test_auth_context (122-146)
  • TestAuthContext (9-22)
backend/app/api/deps.py (3)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-25)
  • TokenPayload (14-15)
backend/app/core/security.py (1)
  • verify (276-337)
backend/app/tests/utils/test_data.py (2)
backend/app/models/api_key.py (1)
  • APIKeyCreateResponse (28-31)
backend/app/crud/api_key.py (2)
  • APIKeyCrud (16-118)
  • create (55-98)
backend/app/tests/api/routes/test_creds.py (2)
backend/app/tests/utils/auth.py (1)
  • TestAuthContext (9-22)
backend/app/tests/conftest.py (2)
  • client (65-68)
  • user_api_key (102-104)
backend/app/models/__init__.py (2)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
backend/app/models/api_key.py (3)
  • APIKey (34-45)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
🔇 Additional comments (8)
backend/app/models/__init__.py (1)

3-5: LGTM!

The new imports for AuthContext and APIKeyCreateResponse correctly expose the authentication context and API key response types needed by the refactored authentication flow.

backend/app/seed_data/seed_data.py (1)

244-246: LGTM!

The credential encryption logic correctly parses the JSON string into a dictionary before encrypting, aligning with the new encrypt_credentials function signature.

backend/app/api/deps.py (1)

150-204: LGTM!

The new get_auth_context function correctly:

  • Verifies API keys or JWT tokens
  • Returns AuthContext with user, organization, and project info (when available)
  • Validates that all linked resources (user, organization, project) are active
  • Properly handles both authentication methods with appropriate error responses
backend/app/tests/conftest.py (4)

83-86: LGTM!

The fixture correctly retrieves the superuser authentication context and returns the API key header. The removal of explicit scope="function" is acceptable as pytest fixtures default to function scope.


89-92: LGTM!

The fixture correctly retrieves the user authentication context and returns the API key header. The implementation is consistent with the superuser_api_key_header fixture.


95-98: LGTM!

The fixture now returns a complete TestAuthContext instead of just APIKeyPublic, providing tests with richer authentication context including user, project, organization, and API key details. This aligns well with the new authentication model.


101-104: LGTM!

The fixture now returns a complete TestAuthContext, consistent with the superuser_api_key fixture. This provides tests with comprehensive authentication context.

backend/app/tests/api/routes/test_creds.py (1)

15-15: LGTM! Type annotations properly updated.

All test function signatures have been correctly updated to use TestAuthContext instead of APIKeyPublic. The type annotation changes are consistent with the new authentication model, and all attribute accesses (.key, .project_id, .organization_id, etc.) remain valid since TestAuthContext provides all required attributes. The changes adhere to the coding guidelines requiring type hints in Python code.

Also applies to: 20-20, 64-64, 90-90, 119-119, 134-134, 150-150, 167-167, 195-195, 222-222, 249-249, 266-266, 292-292, 313-313, 337-337, 403-403, 428-428

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
backend/app/core/security.py (4)

15-15: Duplicate: Update to modern type annotation.

This issue was already flagged in a previous review. The code still uses the deprecated typing.Tuple instead of the built-in tuple type hint.


218-218: Duplicate: Update to modern type annotation.

This method signature still uses Tuple from typing instead of the built-in tuple. This was already flagged in a previous review.


243-243: Duplicate: Update to modern type annotation.

This method signature still uses Tuple from typing instead of the built-in tuple. This was already flagged in a previous review.


276-289: Duplicate: Fix incorrect docstring for return type.

The docstring's "Returns" section was already flagged in a previous review as incorrect—it claims the method returns a tuple while it actually returns AuthContext | None.

🧹 Nitpick comments (1)
backend/app/core/security.py (1)

227-233: Consider generating sufficient bytes to avoid padding.

The current approach slices and pads tokens, which slightly reduces entropy when padding occurs. Padding with "A" is deterministic and predictable.

Apply this diff to generate sufficient bytes upfront:

-    PREFIX_BYTES = 16  # Generates 22 chars in urlsafe base64
-    SECRET_BYTES = 32  # Generates 43 chars in urlsafe base64
+    PREFIX_BYTES = 17  # Generates 23 chars in urlsafe base64, slice to 22
+    SECRET_BYTES = 33  # Generates 44 chars in urlsafe base64, slice to 43

Then update the generate method:

     # Generate tokens and ensure exact length
     secret_length = cls.KEY_LENGTH - cls.PREFIX_LENGTH
-    key_prefix = secrets.token_urlsafe(cls.PREFIX_BYTES)[: cls.PREFIX_LENGTH].ljust(
-        cls.PREFIX_LENGTH, "A"
-    )
-    secret_key = secrets.token_urlsafe(cls.SECRET_BYTES)[:secret_length].ljust(
-        secret_length, "A"
-    )
+    key_prefix = secrets.token_urlsafe(cls.PREFIX_BYTES)[: cls.PREFIX_LENGTH]
+    secret_key = secrets.token_urlsafe(cls.SECRET_BYTES)[:secret_length]

This eliminates the need for padding while maintaining exact lengths through slicing only.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c19da66 and 417c15c.

📒 Files selected for processing (2)
  • backend/app/core/security.py (2 hunks)
  • backend/app/seed_data/seed_data.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/seed_data/seed_data.json
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/core/security.py
backend/app/core/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Place core functionality (config, DB session, security, exceptions, middleware) in backend/app/core/

Files:

  • backend/app/core/security.py
🧬 Code graph analysis (1)
backend/app/core/security.py (5)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/user.py (1)
  • User (48-50)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-57)
backend/app/models/auth.py (1)
  • AuthContext (18-25)
🪛 Ruff (0.14.0)
backend/app/core/security.py

15-15: typing.Tuple is deprecated, use tuple instead

(UP035)

🔇 Additional comments (2)
backend/app/core/security.py (2)

299-324: LGTM: Efficient single-query approach with eager loading.

The verification logic efficiently fetches the APIKey and all related entities (User, Organization, Project) in a single joined query, avoiding N+1 query issues. The query correctly filters by key_prefix and is_deleted status, and the explicit joins ensure that all required relationships are validated at the database level.


326-337: LGTM: Secure verification with proper error handling.

The bcrypt verification is correctly implemented and resistant to timing attacks. The error handling with logging provides good observability without exposing sensitive information. Returning None on any exception maintains a consistent interface.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/app/alembic/versions/e7c68e43ce6f_refactor_api_key_table.py (1)

22-64: Add missing return type hints.

Both upgrade() and downgrade() need -> None annotations to comply with our Python guidelines. Apply the following diff (line numbers refer to this file):

-def upgrade():
+def upgrade() -> None:
@@
-def downgrade():
+def downgrade() -> None:

As per coding guidelines.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 417c15c and 8ce8a12.

📒 Files selected for processing (2)
  • backend/app/alembic/versions/e7c68e43ce6f_refactor_api_key_table.py (1 hunks)
  • backend/app/tests/utils/test_data.py (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/tests/utils/test_data.py
  • backend/app/alembic/versions/e7c68e43ce6f_refactor_api_key_table.py
🧬 Code graph analysis (2)
backend/app/tests/utils/test_data.py (3)
backend/app/models/api_key.py (1)
  • APIKeyCreateResponse (28-31)
backend/app/crud/api_key.py (2)
  • APIKeyCrud (16-118)
  • create (55-98)
backend/app/tests/conftest.py (1)
  • db (28-45)
backend/app/alembic/versions/e7c68e43ce6f_refactor_api_key_table.py (1)
backend/app/alembic/migrate_api_key.py (2)
  • migrate_api_keys (34-154)
  • verify_migration (157-198)
🪛 GitHub Actions: AI Platform CI
backend/app/alembic/versions/e7c68e43ce6f_refactor_api_key_table.py

[error] 1-1: Black formatting: reformatted by Black. Run 'pre-commit run --all-files' or 'black' to fix formatting issues.

🔇 Additional comments (2)
backend/app/tests/utils/test_data.py (2)

7-7: LGTM! Import additions support the refactored API key creation.

The new imports for APIKeyCreateResponse and APIKeyCrud are correctly placed and necessary for the updated create_test_api_key function.

Also applies to: 24-24


86-106: LGTM! Refactored test helper correctly uses the new CRUD pattern and Pydantic v2 API.

The function properly:

  • Uses type hints throughout (satisfies coding guideline for Python 3.11+)
  • Creates missing dependencies (user/project) for test flexibility
  • Delegates API key creation to APIKeyCrud
  • Uses model_dump() at line 106, which is the correct Pydantic v2 method (the past review concern about dict() has been addressed)
  • Returns the appropriate APIKeyCreateResponse containing the raw key and API key metadata

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
backend/app/core/security.py (1)

15-15: Update to use modern type annotation.

The code uses the deprecated typing.Tuple instead of the built-in tuple type hint.

Apply this diff:

-from typing import Any, Tuple
+from typing import Any

Then update the return type annotations to use lowercase tuple:

  • Line 218: def generate(cls) -> tuple[str, str, str]:
  • Line 243: def _extract_key_parts(cls, raw_key: str) -> tuple[str, str] | None:
backend/app/api/deps.py (1)

93-97: Remove redundant organization validation.

The api_key_manager.verify() already validates that the organization exists and eagerly loads it. The validate_organization() call performs a redundant database query.

Apply this diff:

     api_key = request.headers.get("X-API-KEY")
     if api_key:
         api_key_record = api_key_manager.verify(session, api_key)
         if api_key_record:
-            validate_organization(session, api_key_record.organization.id)
             organization_id = api_key_record.organization.id
backend/app/api/permissions.py (1)

18-42: Allow superusers to bypass all permission checks.

Currently, superusers authenticating via JWT (without organization_id/project_id) will fail REQUIRE_ORGANIZATION and REQUIRE_PROJECT checks, regressing superuser access on routes guarded by those permissions.

Apply this diff to short-circuit superusers:

 def has_permission(
     auth_context: AuthContext,
     permission: Permission,
     session: Session | None = None,
 ) -> bool:
     """
     Check if the auth_context has the specified permission.

     Args:
         user_context: The authenticated user context
         permission: The permission to check (Permission enum)
         session: Database session (optional)

     Returns:
         bool: True if user has permission, False otherwise
     """
+    # Superusers bypass all permission checks
+    if auth_context.user.is_superuser:
+        return True
+
     match permission:
         case Permission.SUPERUSER:
             return auth_context.user.is_superuser
         case Permission.REQUIRE_ORGANIZATION:
             return auth_context.organization is not None
         case Permission.REQUIRE_PROJECT:
             return auth_context.project is not None
         case _:
             return False
backend/app/api/routes/api_keys.py (1)

19-43: Security: Use authenticated user's ID instead of accepting arbitrary user_id.

The route accepts user_id as a parameter, allowing callers to create API keys for any user, even with SUPERUSER permission. This violates the principle of least privilege.

Apply this diff to fix the security issue:

 def create_api_key_route(
     project_id: int,
-    user_id: int,
     current_user: AuthContextDep,
     session: SessionDep,
 ):
     """
     Create a new API key for the project and user, Restricted to Superuser.

     The raw API key is returned only once during creation.
     Store it securely as it cannot be retrieved again.
     """
     api_key_crud = APIKeyCrud(session=session, project_id=project_id)
     raw_key, api_key = api_key_crud.create(
-        user_id=user_id,
+        user_id=current_user.user.id,
         project_id=project_id,
     )
🧹 Nitpick comments (1)
backend/app/api/permissions.py (1)

21-21: Remove unused session parameter.

The session parameter is declared but never used in has_permission. If it's intended for future use, add a comment explaining its purpose; otherwise, remove it.

Apply this diff if the parameter is not needed:

 def has_permission(
     auth_context: AuthContext,
     permission: Permission,
-    session: Session | None = None,
 ) -> bool:

And update the docstring and permission_checker call accordingly (line 59).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 890bbd5 and fb7fe60.

📒 Files selected for processing (7)
  • backend/app/api/deps.py (5 hunks)
  • backend/app/api/permissions.py (1 hunks)
  • backend/app/api/routes/api_keys.py (1 hunks)
  • backend/app/core/security.py (2 hunks)
  • backend/app/models/auth.py (2 hunks)
  • backend/app/tests/api/test_permissions.py (1 hunks)
  • backend/app/tests/core/test_security.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/app/tests/api/test_permissions.py
  • backend/app/models/auth.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/core/security.py
  • backend/app/tests/core/test_security.py
  • backend/app/api/deps.py
  • backend/app/api/routes/api_keys.py
  • backend/app/api/permissions.py
backend/app/core/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Place core functionality (config, DB session, security, exceptions, middleware) in backend/app/core/

Files:

  • backend/app/core/security.py
backend/app/api/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Expose FastAPI REST endpoints under backend/app/api/ organized by domain

Files:

  • backend/app/api/deps.py
  • backend/app/api/routes/api_keys.py
  • backend/app/api/permissions.py
🧬 Code graph analysis (5)
backend/app/core/security.py (5)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/user.py (1)
  • User (48-50)
backend/app/models/organization.py (1)
  • Organization (34-54)
backend/app/models/project.py (1)
  • Project (29-57)
backend/app/models/auth.py (1)
  • AuthContext (18-21)
backend/app/tests/core/test_security.py (5)
backend/app/core/security.py (4)
  • APIKeyManager (191-334)
  • generate (218-240)
  • _extract_key_parts (243-273)
  • verify (276-334)
backend/app/models/api_key.py (1)
  • APIKey (34-45)
backend/app/models/auth.py (1)
  • AuthContext (18-21)
backend/app/tests/utils/test_data.py (1)
  • create_test_api_key (86-106)
backend/app/tests/conftest.py (1)
  • db (28-45)
backend/app/api/deps.py (4)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-21)
  • TokenPayload (14-15)
backend/app/models/user.py (3)
  • User (48-50)
  • UserOrganization (53-55)
  • UserProjectOrg (58-59)
backend/app/core/security.py (1)
  • verify (276-334)
backend/app/api/routes/api_keys.py (5)
backend/app/crud/api_key.py (4)
  • APIKeyCrud (16-118)
  • create (55-98)
  • read_all (38-53)
  • delete (100-118)
backend/app/models/api_key.py (2)
  • APIKeyPublic (21-25)
  • APIKeyCreateResponse (28-31)
backend/app/models/message.py (1)
  • Message (5-6)
backend/app/utils.py (2)
  • APIResponse (29-53)
  • success_response (36-39)
backend/app/api/permissions.py (2)
  • Permission (10-15)
  • require_permission (45-70)
backend/app/api/permissions.py (1)
backend/app/models/auth.py (1)
  • AuthContext (18-21)
🪛 Ruff (0.14.0)
backend/app/core/security.py

15-15: typing.Tuple is deprecated, use tuple instead

(UP035)

backend/app/api/routes/api_keys.py

22-22: Unused function argument: current_user

(ARG001)

backend/app/api/permissions.py

21-21: Unused function argument: session

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (10)
backend/app/core/security.py (4)

265-270: LGTM! Backward compatibility for old key format.

The old key format support (43 chars with 12-char prefix) is properly implemented, ensuring smooth migration from the legacy system.


299-310: LGTM! Efficient single-query authentication.

The join strategy efficiently loads all related entities (User, Organization, Project) in a single query, avoiding N+1 problems.


329-334: LGTM! Proper error handling with logging.

The try-except block properly catches exceptions, logs them with context, and returns None, preventing information leakage while maintaining observability.


226-233: Padding never triggers—the code is secure as written.

The constants are perfectly aligned with token_urlsafe output lengths:

  • token_urlsafe(16) produces exactly 22 chars; slicing to PREFIX_LENGTH (22) yields the full string, so ljust(22, "A") is a no-op.
  • token_urlsafe(32) produces exactly 43 chars; slicing to secret_length (43) yields the full string, so ljust(43, "A") is a no-op.

The padding with 'A' is defensive but will never execute, so there is no entropy loss in practice. The code is correct.

backend/app/tests/core/test_security.py (1)

129-298: LGTM! Comprehensive test coverage for APIKeyManager.

The test suite thoroughly covers:

  • Key generation (format, length, uniqueness, bcrypt hashing)
  • Key extraction for both new (65-char) and old (43-char) formats
  • Verification with valid keys, invalid keys, wrong secrets, deleted keys, and malformed inputs
  • Constants validation
  • Integration testing

The tests are well-structured and provide strong confidence in the APIKeyManager implementation.

backend/app/api/deps.py (3)

47-58: LGTM! Proper API key authentication flow.

The function correctly uses api_key_manager.verify to authenticate and retrieve the user from the AuthContext.


60-78: LGTM! JWT authentication fallback.

The elif branch properly handles JWT token authentication with appropriate error handling for invalid tokens and inactive users.


150-174: LGTM! Comprehensive authentication with proper validation.

The function properly:

  • Authenticates via API key or JWT token
  • Validates user, organization, and project are active
  • Returns a complete AuthContext for downstream use

The error handling is appropriate with specific status codes for different failure scenarios.

backend/app/api/routes/api_keys.py (2)

46-66: LGTM! Proper pagination and security for listing API keys.

The route correctly:

  • Enforces project-scoped permission
  • Uses pagination with sensible defaults
  • Returns only public key information (prefix, not secret)

69-85: LGTM! Proper project-scoped deletion.

The route correctly:

  • Enforces project-scoped permission
  • Uses the authenticated user's project ID
  • Returns a clear success message

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
backend/app/api/deps.py (2)

94-98: Remove redundant organization validation and avoid extra query.

api_key_manager.verify already joins and returns AuthContext with organization; drop validate_organization() and use the loaded org.

-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization.id)
-            organization_id = api_key_record.organization.id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            organization_id = auth_context.organization.id

115-119: Remove redundant organization validation and avoid extra query.

-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization.id)
-            organization_id = api_key_record.organization.id
-            project_id = api_key_record.project.id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            organization_id = auth_context.organization.id
+            project_id = auth_context.project.id
🧹 Nitpick comments (4)
backend/app/api/deps.py (4)

36-37: Make token and api_key parameters Optional in type hints.

With auto_error=False, both may be None. Use str | None for accuracy and mypy friendliness.

As per coding guidelines.

 SessionDep = Annotated[Session, Depends(get_db)]
-TokenDep = Annotated[str, Depends(reusable_oauth2)]
+TokenDep = Annotated[str | None, Depends(reusable_oauth2)]

 def get_current_user(
     session: SessionDep,
     token: TokenDep,
-    api_key: Annotated[str, Depends(api_key_header)],
+    api_key: Annotated[str | None, Depends(api_key_header)],
 ) -> User:
@@
 def get_auth_context(
     session: SessionDep,
     token: TokenDep,
-    api_key: Annotated[str, Depends(api_key_header)],
+    api_key: Annotated[str | None, Depends(api_key_header)],
 ) -> AuthContext:

Also applies to: 43-44, 153-154


75-76: Use 403 for inactive user (consistency with get_auth_context).

-        if not user.is_active:
-            raise HTTPException(status_code=400, detail="Inactive user")
+        if not user.is_active:
+            raise HTTPException(status_code=403, detail="Inactive user")

155-159: *Fix docstring: AuthContext contains objects, not _id fields.

-    Verify valid authentication (API Key or JWT token) and return authenticated user context.
-    Returns AuthContext with user info, project_id, and organization_id.
-    Authorization logic should be handled in routes.
+    Verify valid authentication (API Key or JWT token) and return authenticated user context.
+    Returns AuthContext with user, organization, and project objects.
+    Authorization should be handled in routes.

168-173: Defensive None checks for org/project before is_active access.

AuthContext types are Optional; guard against None to prevent AttributeError if future changes return partial contexts.

-        if not auth_context.organization.is_active:
+        if not auth_context.organization or not auth_context.organization.is_active:
             raise HTTPException(status_code=403, detail="Inactive Organization")
 
-        if not auth_context.project.is_active:
+        if not auth_context.project or not auth_context.project.is_active:
             raise HTTPException(status_code=403, detail="Inactive Project")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb7fe60 and 645b510.

📒 Files selected for processing (1)
  • backend/app/api/deps.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/api/deps.py
backend/app/api/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Expose FastAPI REST endpoints under backend/app/api/ organized by domain

Files:

  • backend/app/api/deps.py
🧬 Code graph analysis (1)
backend/app/api/deps.py (4)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-21)
  • TokenPayload (14-15)
backend/app/models/user.py (3)
  • User (48-50)
  • UserOrganization (53-55)
  • UserProjectOrg (58-59)
backend/app/core/security.py (1)
  • verify (276-334)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)
🔇 Additional comments (1)
backend/app/api/deps.py (1)

121-123: Confirm API key requirement for org/project path.

JWT branch is not allowed here. Is this intentional? If JWT users should access org/project-scoped routes via membership, consider a JWT fallback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
backend/app/api/deps.py (3)

91-94: Fix variable name and remove redundant organization validation.

Two issues:

  1. The variable api_key_record is misleading—api_key_manager.verify() returns AuthContext | None, not an APIKey record.
  2. The validate_organization() call on line 93 is redundant. Per the relevant code snippets, api_key_manager.verify() already validates that the organization exists and is active in a single query, then eagerly loads it into the returned AuthContext.

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization.id)
-            organization_id = api_key_record.organization.id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            organization_id = auth_context.organization.id

112-116: Fix variable name and remove redundant organization validation.

Two issues:

  1. The variable api_key_record is misleading—api_key_manager.verify() returns AuthContext | None, not an APIKey record.
  2. The validate_organization() call on line 114 is redundant. Per the relevant code snippets, api_key_manager.verify() already validates that the organization exists and is active in a single query, then eagerly loads it into the returned AuthContext.

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if api_key_record:
-            validate_organization(session, api_key_record.organization.id)
-            organization_id = api_key_record.organization.id
-            project_id = api_key_record.project.id
+        auth_context = api_key_manager.verify(session, api_key)
+        if auth_context:
+            organization_id = auth_context.organization.id
+            project_id = auth_context.project.id

48-55: Fix misleading variable name.

The variable api_key_record is misleading since api_key_manager.verify() returns AuthContext | None, not an APIKey record. This creates confusion and is inconsistent with the naming in get_auth_context (line 158).

Apply this diff:

     if api_key:
-        api_key_record = api_key_manager.verify(session, api_key)
-        if not api_key_record:
+        auth_context = api_key_manager.verify(session, api_key)
+        if not auth_context:
             raise HTTPException(status_code=401, detail="Invalid API Key")

-        if not api_key_record.user.is_active:
+        if not auth_context.user.is_active:
             raise HTTPException(status_code=403, detail="Inactive user")

-        return api_key_record.user  # Return only User object
+        return auth_context.user  # Return only User object
🧹 Nitpick comments (1)
backend/app/api/deps.py (1)

165-169: Consider adding defensive None checks.

While api_key_manager.verify() guarantees that organization and project are populated when it returns a non-None AuthContext (per the join logic in the relevant code snippet), the AuthContext model defines these fields as optional (Organization | None and Project | None). Adding explicit None checks before accessing .is_active would be defensive and guard against future refactoring.

Apply this diff:

         if not auth_context.user.is_active:
             raise HTTPException(status_code=403, detail="Inactive user")

-        if not auth_context.organization.is_active:
+        if auth_context.organization and not auth_context.organization.is_active:
             raise HTTPException(status_code=403, detail="Inactive Organization")

-        if not auth_context.project.is_active:
+        if auth_context.project and not auth_context.project.is_active:
             raise HTTPException(status_code=403, detail="Inactive Project")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 645b510 and 34632a1.

📒 Files selected for processing (1)
  • backend/app/api/deps.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use type hints in Python code (Python 3.11+ project)

Files:

  • backend/app/api/deps.py
backend/app/api/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Expose FastAPI REST endpoints under backend/app/api/ organized by domain

Files:

  • backend/app/api/deps.py
🧬 Code graph analysis (1)
backend/app/api/deps.py (4)
backend/app/crud/organization.py (1)
  • validate_organization (40-57)
backend/app/models/auth.py (2)
  • AuthContext (18-21)
  • TokenPayload (14-15)
backend/app/models/user.py (3)
  • User (48-50)
  • UserOrganization (53-55)
  • UserProjectOrg (58-59)
backend/app/core/security.py (1)
  • verify (276-334)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks (3.11.7, 6)

@avirajsingh7 avirajsingh7 merged commit 3b7b640 into main Oct 17, 2025
3 checks passed
@avirajsingh7 avirajsingh7 deleted the refactor/authentication branch October 17, 2025 03:08
@sentry-io
Copy link

sentry-io bot commented Oct 17, 2025

Issues attributed to commits in this pull request

This pull request was merged and Sentry observed the following issues:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ready-for-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inefficient and insecure API key lookup in get_api_key_by_value

3 participants