Skip to content

Add API key scopes, expiry, and production storage guards#176

Open
webhop123 wants to merge 2 commits into
XortexAI:mainfrom
webhop123:fix/api-key-production-guards
Open

Add API key scopes, expiry, and production storage guards#176
webhop123 wants to merge 2 commits into
XortexAI:mainfrom
webhop123:fix/api-key-production-guards

Conversation

@webhop123
Copy link
Copy Markdown

Summary

  • store scopes, optional expiry, and org/project bindings on API keys
  • enforce scope, binding, and expiry checks during API key validation
  • refuse in-memory user/API-key fallback when ENVIRONMENT=production

Refs #161

Tests

  • .\.venv\Scripts\python -m ruff check src\database\api_key_store.py src\database\user_store.py src\api\routes\api_keys.py src\api\dependencies.py tests\unit\test_database_stores.py
  • .\.venv\Scripts\python -m pytest tests\unit\test_database_stores.py tests\api\test_dependencies_and_routes.py -q

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request enhances the API key system by introducing support for scopes, expiration timestamps, and organization/project bindings across the API routes and database layers. It also implements a security measure to disable in-memory storage fallbacks in production environments. Feedback highlights a potential runtime error due to naive and aware datetime comparisons, a logic flaw in how bound keys are validated against request contexts, and the addition of unused database indexes that increase write overhead.

Comment thread src/database/api_key_store.py Outdated

def _is_expired(self, key_doc: Dict[str, Any]) -> bool:
expires_at = key_doc.get("expires_at")
return bool(expires_at and datetime.utcnow() > expires_at)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The comparison between datetime.utcnow() (which is offset-naive) and expires_at will raise a TypeError if expires_at is an offset-aware datetime. Pydantic parses ISO 8601 strings with timezone offsets (like 'Z' or '+00:00') as aware datetimes, making this a likely runtime error when validating keys created via the API. Additionally, datetime.utcnow() is deprecated in Python 3.12. It is recommended to use aware datetimes consistently throughout the application (e.g., datetime.now(timezone.utc)).

Comment thread src/database/api_key_store.py Outdated
Comment on lines +122 to +125
if org_id is not None and bound_org not in (None, org_id):
return False
if project_id is not None and bound_project not in (None, project_id):
return False
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current binding logic allows a restricted API key (where org_id or project_id is set) to be used if the request context does not provide these identifiers (i.e., they are None). This bypasses the intended restriction. If a key is bound to a specific organization or project, it should be rejected if used in a context where that binding cannot be verified.

Suggested change
if org_id is not None and bound_org not in (None, org_id):
return False
if project_id is not None and bound_project not in (None, project_id):
return False
if bound_org is not None and bound_org != org_id:
return False
if bound_project is not None and bound_project != project_id:
return False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@webhop123 please have a look on this suggestion

Comment thread src/database/api_key_store.py Outdated
Comment on lines +84 to +85
self.api_keys.create_index([("expires_at", ASCENDING)])
self.api_keys.create_index([("org_id", ASCENDING), ("project_id", ASCENDING)])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

These indexes on expires_at, org_id, and project_id are not currently utilized by any queries in this class. The validate_api_key method performs a lookup by key_hash and filters the other fields in Python. Unless these indexes are intended for external tools or future query patterns, they add unnecessary overhead to write operations and should be removed.

@webhop123 webhop123 marked this pull request as ready for review May 11, 2026 19:57
@webhop123
Copy link
Copy Markdown
Author

Bound keys now require a matching org/project context, expiry checks normalize datetimes to UTC, and the unused indexes were removed. I also added coverage for the binding and expiry cases.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants