Skip to content

Feat: add API key management system#9177

Open
Shylesh1640 wants to merge 2 commits intofossasia:developmentfrom
Shylesh1640:api-key-management
Open

Feat: add API key management system#9177
Shylesh1640 wants to merge 2 commits intofossasia:developmentfrom
Shylesh1640:api-key-management

Conversation

@Shylesh1640
Copy link
Copy Markdown

@Shylesh1640 Shylesh1640 commented Apr 3, 2026

What

  • Added API Key Management (create, list, revoke API keys per user)
  • Implemented per-user rate limiting (JWT identity with IP fallback)
  • Added configurable RATE_LIMIT_DEFAULTS

Why

  • Enable integrations to authenticate using stable API keys
  • Improve fairness and control over request limits

How

  • Created ApiKey model, schema, routes, and migration
  • Updated limiter to prefer user ID when JWT is present

Testing

  • Manual testing recommended
  • Steps:
    • Run: python manage.py db upgrade
    • Create and list API keys via API endpoints

Summary

This PR introduces API key management and enhances rate limiting by shifting from IP-based to user-based identification when available.

Summary by Sourcery

Introduce per-user API rate limiting and a new API key management system, including persistence, schema, and REST endpoints, plus configurable default rate limits and minor Docker build updates.

New Features:

  • Add ApiKey model, schema, and Alembic migration to store user-scoped API keys with hashing and revocation metadata.
  • Expose authenticated endpoints to create, list, view, revoke, and delete API keys, including user relationships and JSON:API wiring.
  • Allow configuring global default rate limits via the RATE_LIMIT_DEFAULTS environment variable and application config.

Enhancements:

  • Change rate limiting to prefer a stable per-user key based on JWT identity, falling back to client IP when no user is available.
  • Extend user API schema and routing to surface API key relationships from user resources.
  • Update Docker build to use a pinned Poetry version via pip and install wget as a build dependency.

Build:

  • Adjust Dockerfile dependencies and Poetry installation method for more reliable image builds.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 3, 2026

Reviewer's Guide

Implements a full API key management system (model, schema, routes, migration) with per-user JWT-aware rate limiting and configurable default rate limits, plus minor Docker build changes for Poetry installation.

Sequence diagram for creating a new API key via POST /api-keys

sequenceDiagram
    actor Client
    participant ApiKeyListPost
    participant PermissionManager as PermissionManager
    participant ApiKeyModel as ApiKey
    participant DB as Database

    Client->>ApiKeyListPost: POST /api-keys (user relationship)
    ApiKeyListPost->>PermissionManager: has_access is_user_itself(user_id)
    PermissionManager-->>ApiKeyListPost: allowed / forbidden
    ApiKeyListPost-->>Client: 403 Forbidden
    Note over Client,ApiKeyListPost: if forbidden

    ApiKeyListPost->>ApiKeyModel: generate_token()
    ApiKeyModel-->>ApiKeyListPost: raw_token
    ApiKeyListPost->>ApiKeyModel: hash_token(raw_token)
    ApiKeyModel-->>ApiKeyListPost: token_hash
    ApiKeyListPost->>DB: INSERT api_keys (token_hash, prefix, user_id, name)
    DB-->>ApiKeyListPost: created ApiKey
    ApiKeyListPost->>ApiKeyModel: set token on instance
    ApiKeyListPost-->>Client: 201 Created (token, prefix, metadata)
Loading

ER diagram for new api_keys table and relation to users

erDiagram
    USERS {
        int id PK
        string email
    }

    API_KEYS {
        int id PK
        string name
        string token_hash UK
        string prefix
        datetime last_used_at
        datetime revoked_at
        datetime created_at
        int user_id FK
        datetime deleted_at
    }

    USERS ||--o{ API_KEYS : has
Loading

Class diagram for new ApiKey domain and API resources

classDiagram
    class ApiKey {
        <<SQLAlchemyModel>>
        +int id
        +str name
        +str token_hash
        +str prefix
        +datetime last_used_at
        +datetime revoked_at
        +datetime created_at
        +int user_id
        +str token
        +static str generate_token()
        +static str hash_token(token)
        +static str get_service_name()
    }

    class User {
        <<SQLAlchemyModel>>
        +int id
        +str email
        +str password
        +List~ApiKey~ api_keys
    }

    class ApiKeySchema {
        <<MarshmallowSchema>>
        +int id
        +str name
        +str prefix
        +str token
        +str token_hash
        +datetime created_at
        +datetime last_used_at
        +datetime revoked_at
        +Relationship user
    }

    class ApiKeyListPost {
        <<ResourceList>>
        +before_post(args, kwargs, data)
        +before_create_object(data, view_kwargs)
        +after_create_object(api_key, data, view_kwargs)
    }

    class ApiKeyList {
        <<ResourceList>>
        +query(view_kwargs)
    }

    class ApiKeyDetail {
        <<ResourceDetail>>
        +before_get(args, kwargs)
        +before_update_object(api_key, data, view_kwargs)
    }

    class ApiKeyRelationship {
        <<ResourceRelationship>>
    }

    ApiKey --> User : user
    User "1" --> "*" ApiKey : api_keys

    ApiKeySchema --> ApiKey : maps
    ApiKeyListPost --> ApiKeySchema : uses
    ApiKeyList --> ApiKeySchema : uses
    ApiKeyDetail --> ApiKeySchema : uses
    ApiKeyRelationship --> ApiKeySchema : uses

    ApiKeyListPost --> ApiKey : creates
    ApiKeyList --> ApiKey : queries
    ApiKeyDetail --> ApiKey : reads_updates_deletes
    ApiKeyRelationship --> ApiKey : manages_relationships
Loading

Flow diagram for rate limit key selection with JWT and IP fallback

flowchart TD
    A[Incoming request] --> B[Call rate_limit_key]
    B --> C[Try get_identity]
    C --> D{User object with id available?}
    D -- Yes --> E[Return key user:id]
    D -- No --> F[Call get_ipaddr]
    F --> G[Return client IP as key]
Loading

File-Level Changes

Change Details Files
Add per-user rate limiting that prefers authenticated user identity over IP and supports configurable default limits.
  • Introduce rate_limit_key helper that uses JWT identity when available and falls back to IP address
  • Wire Limiter to use rate_limit_key as key_func instead of plain IP-based keying
  • Read RATE_LIMIT_DEFAULTS from app config and assign them to limiter.default_limits during initialization
  • Parse RATE_LIMIT_DEFAULTS from environment in Config into a list of limit strings
app/extensions/limiter.py
config.py
Introduce API key persistence model and database migration.
  • Create ApiKey SQLAlchemy model with hashed token storage, prefix, timestamps, and user relationship
  • Add helper methods to ApiKey for token generation, hashing, and service naming
  • Add Alembic migration to create api_keys table with indices, foreign key to users, and uniqueness on token_hash
app/models/api_key.py
migrations/versions/rev-2026-04-03-120000-8f7c2b9e5a12_add_api_keys.py
Expose API key CRUD and relationship endpoints secured by JWT and ownership checks.
  • Add ApiKeyListPost to create API keys for the authenticated user, generating and storing only a hash while returning the raw token once
  • Add ApiKeyList to list a user’s own API keys or, for admins, list all keys, enforcing access control
  • Add ApiKeyDetail to fetch, revoke (via revoked_at), and delete keys with strong validation preventing mutation of immutable fields and double revocation
  • Add ApiKeyRelationship resource to manage API key relationships
  • Register new API key routes and user relationships in the main API routing module and user schema
app/api/api_keys.py
app/api/routes.py
app/api/schema/api_keys.py
app/api/schema/users.py
Adjust Docker build to use system pip for a pinned Poetry version and install wget as a build dependency.
  • Add wget to Alpine build dependencies
  • Replace remote installer script for Poetry with direct pip installation of a specific Poetry version
Dockerfile

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The ApiKeyDetail resource allows DELETE but does not perform any ownership or role check before deletion; consider adding a before_delete_object (or before_delete) hook mirroring the before_get/before_update_object access controls so users cannot delete keys they do not own.
  • In ApiKeyListPost.before_post, data['user'] is passed directly as user_id, but for JSON:API relationships this is typically an object (e.g. {data: {id: ...}}); verify and normalize the payload shape before calling has_access('is_user_itself', user_id=...) to avoid incorrect authorization decisions.
  • The user relationship in ApiKeySchema uses related_view_kwargs={'api_key_id': '<id>'}, but the user detail route appears to be keyed by id rather than api_key_id; aligning this kwarg with the actual route signature will avoid URL generation errors for the relationship.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `ApiKeyDetail` resource allows `DELETE` but does not perform any ownership or role check before deletion; consider adding a `before_delete_object` (or `before_delete`) hook mirroring the `before_get`/`before_update_object` access controls so users cannot delete keys they do not own.
- In `ApiKeyListPost.before_post`, `data['user']` is passed directly as `user_id`, but for JSON:API relationships this is typically an object (e.g. `{data: {id: ...}}`); verify and normalize the payload shape before calling `has_access('is_user_itself', user_id=...)` to avoid incorrect authorization decisions.
- The `user` relationship in `ApiKeySchema` uses `related_view_kwargs={'api_key_id': '<id>'}`, but the user detail route appears to be keyed by `id` rather than `api_key_id`; aligning this kwarg with the actual route signature will avoid URL generation errors for the relationship.

## Individual Comments

### Comment 1
<location path="app/extensions/limiter.py" line_range="9-11" />
<code_context>
+
+def rate_limit_key():
+    user = None
+    try:
+        user = get_identity()
+    except Exception:
+        user = None
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Catching bare `Exception` in the rate limit key function can hide unexpected errors.

This will hide real bugs in `get_identity()` by silently falling back to IP-based limiting, making diagnosis harder and changing behavior unexpectedly. Prefer catching only the specific exception(s) raised when no/invalid JWT is present and let other exceptions propagate.

Suggested implementation:

```python
from flask_limiter import Limiter
from flask_limiter.util import get_ipaddr
from flask_jwt_extended.exceptions import JWTExtendedException

from app.api.helpers.jwt import get_identity


def rate_limit_key():
    user = None
    try:
        user = get_identity()
    except JWTExtendedException:
        # No or invalid JWT; fall back to IP-based limiting
        user = None

```

If your project uses a different exception type to signal “no/invalid JWT” in `get_identity()`, replace `JWTExtendedException` with that specific exception and adjust the import accordingly. Also ensure that `rate_limit_key` ultimately returns either a user identifier or the IP address (e.g. `return user or get_ipaddr()`), matching how the rest of the file/config is structured.
</issue_to_address>

### Comment 2
<location path="app/api/schema/api_keys.py" line_range="39" />
<code_context>
+        self_view='v1.api_key_user',
+        self_view_kwargs={'id': '<id>'},
+        related_view='v1.user_detail',
+        related_view_kwargs={'api_key_id': '<id>'},
+        schema='UserSchema',
+        type_='user',
</code_context>
<issue_to_address>
**issue (bug_risk):** `related_view_kwargs` for the `user` relationship likely targets the wrong parameter name.

The `user` relationship targets `v1.user_detail`, but `related_view_kwargs` uses `{'api_key_id': '<id>'}`. Unless `user_detail` is defined to accept `api_key_id`, this link won’t resolve. You likely want to pass the user identifier instead (for example `{'id': '<user_id>'}`), aligned with how `user_detail` is wired.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread app/extensions/limiter.py Outdated
Comment thread app/api/schema/api_keys.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant