Skip to content

feat: add scoped API keys and conditional item updates - #202

Open
krzysiek-kurek wants to merge 6 commits into
Anyesh:mainfrom
krzysiek-kurek:feat/scoped-api-keys-conditional-updates
Open

krzysiek-kurek wants to merge 6 commits into
Anyesh:mainfrom
krzysiek-kurek:feat/scoped-api-keys-conditional-updates

Conversation

@krzysiek-kurek

@krzysiek-kurek krzysiek-kurek commented Sep 17, 2026

Copy link
Copy Markdown

Description

Adds scoped API keys for programmatic access to Wardrowbe, a Settings UI for managing them, and optional optimistic concurrency for item updates.

API keys are user-owned and support these scopes:

  • items:read
  • items:write
  • images:read

The plaintext token is returned only once when the key is created. Only its SHA-256 hash is stored. Keys can expire, be revoked, and revoked or expired keys can be permanently deleted.

A new Settings → API Keys section lets users:

  • create API keys and select scopes,
  • optionally configure expiration,
  • copy the token when it is first created,
  • view key status and last-used metadata,
  • revoke active keys,
  • delete inactive keys.

API-key authentication is supported alongside the existing JWT/session authentication. Existing JWT behavior is unchanged.

Image URLs are only exposed to API keys with the images:read scope.

Item responses now also include an opaque revision. Clients may optionally send that revision using If-Match when updating an item. Stale revisions return 412 Precondition Failed instead of silently overwriting newer changes. Existing unconditional item updates remain supported.

The conditional update implementation uses row locking when If-Match is supplied and supports strong entity tags, comma-separated validators, and the standard * wildcard.

Related Issue

N/A

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/CD or build changes

Checklist

  • I have read the [CONTRIBUTING](CONTRIBUTING.md) guide
  • My code follows the project's coding style
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally
  • I have updated documentation as needed
  • My changes don't introduce new warnings or errors

Testing

Test Environment

  • Docker Compose
  • Kubernetes
  • Local development

Tests Performed

  • Full backend test suite — 718 passed
  • Full frontend test suite — 182 passed
  • ruff check . — passed
  • ruff format --check . — passed
  • Frontend typecheck — passed
  • Frontend production build — passed
  • i18n key, parity, and untranslated-string checks — passed
  • Fresh PostgreSQL migration verification — passed
  • alembic heads — single head
  • Manual API key lifecycle validation on a running deployment
  • Production deployment smoke validation — backend and frontend healthy

Additional regression coverage includes:

  • API key creation, expiration, scope enforcement, revocation and deletion,
  • signed image URL restrictions,
  • monotonic last_used_at updates under concurrent requests,
  • stale conditional updates returning 412,
  • correct handling of quoted and comma-separated If-Match values,
  • API key creation/revocation/deletion UI flows,
  • visible UI feedback when revocation fails.

Screenshots (if applicable)

The new API key management UI is available at Settings → API Keys.

Additional Notes

  • Adds the api_keys table and corresponding Alembic migration.
  • Existing JWT/session authentication remains unchanged.
  • Existing unconditional item updates remain supported.
  • Revocation is intentionally irreversible. A revoked key must be replaced with a newly generated key rather than reactivated.
  • No new frontend dependencies are introduced.

This change was developed with AI assistance (ChatGPT, OpenAI). The implementation and tests were reviewed and verified locally before submission.

Summary by Sourcery

Enable secure scoped API access and prevent stale item updates from overwriting newer changes.

New Features:

  • Add user-owned scoped API keys with one-time token display, expiration, revocation, deletion, usage metadata, and API authentication for item and image access.
  • Add API key management controls to the Settings page, including creation, scope selection, token copying, status visibility, revocation, and deletion.

Enhancements:

  • Add opaque item revisions and optional If-Match conditional updates that reject stale writes with HTTP 412 while preserving unconditional updates.
  • Restrict signed image URLs for API-key requests to keys granted the images:read scope while leaving JWT/session behavior unchanged.

Tests:

  • Add backend and frontend coverage for API key lifecycle, authorization, image URL restrictions, concurrency behavior, and Settings UI flows.

Chores:

  • Add the api_keys database model, schema, service, and migration.

@github-actions github-actions Bot added javascript Pull requests that update javascript code python Pull requests that update python code labels Sep 17, 2026
@sourcery-ai

sourcery-ai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR introduces hashed, user-owned scoped API keys with lifecycle management and Settings UI integration, while preserving JWT authentication and adding optional revision-based conditional item updates with row locking and standard If-Match validation. It also gates signed image URLs by API-key scope and adds broad backend/frontend regression coverage.

Sequence diagram for scoped API key authentication

sequenceDiagram
    participant Client
    participant API
    participant Auth as Auth resolver
    participant Keys as ApiKeyService
    participant DB as Database

    Client->>API: Request with Bearer API key
    API->>Auth: get_current_user
    Auth->>Auth: _required_api_key_scope
    Auth->>Keys: authenticate_with_key(token, required_scope)
    Keys->>DB: Find token_hash
    DB-->>Keys: ApiKey
    Keys->>DB: Validate status, expiry, scope and user
    DB-->>Keys: User
    Keys->>DB: Update last_used_at
    Keys-->>Auth: User and ApiKey
    Auth-->>API: Authenticated request
    API-->>Client: Scoped response
Loading

Sequence diagram for conditional item update

sequenceDiagram
    participant Client
    participant API
    participant Items as ItemService
    participant DB as Database

    Client->>API: PATCH item with If-Match
    API->>Items: get_by_id_for_update(item_id, user_id)
    Items->>DB: SELECT item FOR UPDATE
    DB-->>Items: Current item
    Items-->>API: Locked item
    API->>API: item_revision(item.id, item.updated_at)
    API->>API: if_match_accepts(header, revision)
    alt Revision accepted
        API->>Items: update(item, item_data)
        Items->>DB: Persist update
        DB-->>Items: Updated item
        API-->>Client: 200 ItemResponse with revision
    else Stale revision
        API-->>Client: 412 Precondition Failed
    end
Loading

Entity relationship diagram for user-owned API keys

erDiagram
    USERS ||--o{ API_KEYS : owns
    USERS {
        UUID id PK
    }
    API_KEYS {
        UUID id PK
        UUID user_id FK
        string token_hash UK
        JSONB scopes
        datetime expires_at
        datetime revoked_at
        datetime last_used_at
    }
Loading

File-Level Changes

Change Details Files
Added persistent, scoped API-key lifecycle management and authentication alongside existing JWT/session access.
  • Added the user-owned API key model, schema, service, and database migration.
  • Generated one-time plaintext tokens while storing only SHA-256 hashes; supported expiration, revocation, inactive-key deletion, and monotonic usage timestamps.
  • Added authenticated create, list, revoke, and delete endpoints.
  • Resolved bearer tokens as either API keys or JWTs, enforcing items:read, items:write, and images:read based on request routes.
  • Restricted signed image URLs for API-key responses unless images:read is granted.
backend/app/api/api_keys.py
backend/app/api/router.py
backend/app/models/api_key.py
backend/app/models/__init__.py
backend/app/schemas/api_key.py
backend/app/services/api_key_service.py
backend/app/utils/auth.py
backend/migrations/versions/d6e7f8a9b0c1_add_user_api_keys.py
Implemented opaque item revisions and optional optimistic concurrency for updates.
  • Added a revision derived from item identity and updated timestamp to item responses.
  • Used row-level locking when If-Match is supplied before validating the current revision.
  • Supported strong quoted tags, comma-separated validators, and the * wildcard; stale validators return 412 while unconditional patches remain unchanged.
backend/app/api/items.py
backend/app/schemas/item.py
backend/app/services/item_service.py
backend/app/utils/item_revision.py
Added Settings UI for API-key creation and lifecycle operations.
  • Added API-key creation with scope and optional expiration controls.
  • Displayed the token only after creation with copy and one-time-dismiss behavior.
  • Listed status, scopes, expiration, creation, and last-used metadata; exposed revoke and inactive-key delete actions with failure feedback.
  • Added localized settings strings and integrated the card into the Settings page.
frontend/components/settings/api-keys-card.tsx
frontend/app/dashboard/settings/page.tsx
frontend/messages/de/settings.json
frontend/messages/en/settings.json
frontend/messages/fr/settings.json
frontend/messages/it/settings.json
frontend/messages/ja/settings.json
frontend/messages/ko/settings.json
frontend/messages/zh-CN/settings.json
frontend/messages/zh-TW/settings.json
Added backend and frontend regression coverage for scoped access, concurrency behavior, and UI flows.
  • Covered token secrecy, scope authorization, expiration, revocation, deletion, image URL filtering, concurrent usage/revocation, and last-used ordering.
  • Covered revision generation, stale updates, and If-Match parsing.
  • Covered API-key creation, revoke/delete flows, error feedback, and Settings integration.
backend/tests/test_api_keys.py
backend/tests/test_item_conditional_update.py
frontend/tests/api-keys-card.test.tsx
frontend/tests/settings-api-keys.test.tsx

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

@krzysiek-kurek

Copy link
Copy Markdown
Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="backend/app/services/api_key_service.py" line_range="90-99" />
<code_context>
+        if not token.startswith(API_KEY_PREFIX):
+            return None
+
+        result = await self.db.execute(
+            select(ApiKey).where(ApiKey.token_hash == hash_api_key(token))
+        )
+        api_key = result.scalar_one_or_none()
+        now = datetime.now(UTC)
+        if not api_key or api_key.revoked_at is not None:
+            return None
+        if api_key.expires_at is not None and api_key.expires_at <= now:
+            return None
+        if required_scope not in api_key.scopes:
+            return None
+
</code_context>
<issue_to_address>
**issue (bug_risk):** API-key authentication reads that the key is active, then performs user lookup and updates `last_used_at` in separate statements without rechecking revocation. A concurrent revoke can commit after the initial read but before authentication returns, allowing a request authenticated with the revoked key to complete.

**Triggers:** When a request authenticates concurrently with revocation of the same key.

**Suggested fix:** Make authentication's active-key check and usage update atomic, or lock/recheck the key before returning authenticated access.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and this introduces bearer credentials and a new authorization policy: an incorrect route-to-scope mapping or image capability check could let a key holder read or update data beyond the intended grant. Reverting stops future use, but credentials may already have exposed data or caused updates, and a policy mistake can affect every API-key request without producing an obvious failure.

Blocking findings: backend/app/services/api_key_service.py:99


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread backend/app/services/api_key_service.py
@krzysiek-kurek

Copy link
Copy Markdown
Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. A mistake in API-key scope routing or authentication could grant bearer tokens access to user items, updates, or signed image URLs beyond their intended permissions. Reverting would stop future use, but any data or access exposed before the revert cannot be fully recovered; this also establishes a new authentication trust boundary.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@krzysiek-kurek
krzysiek-kurek marked this pull request as ready for review September 17, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update javascript code python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants