Files
Anthony StirlingandEthanHealy01 357eb77f94 Portal: multiple named personal API keys with per-key usage tracking (#6961)
# Description of Changes

Multiple named **personal** API keys per user, replacing the single
opaque per-user key.

- Create (name + one-time secret), list, and revoke named keys from the
portal Infrastructure → API Keys tab. Works self-hosted and SaaS
(`X-API-KEY`).
- Per-key usage stats (today / trailing 30 days / lifetime);
API-processed documents are attributed to the specific key in the
processor's Documents feed.
- The legacy single per-user key keeps working and is lazily represented
as a named key. Rotating it revokes its migrated shadow row so the old
secret stops authenticating.
- Per-user (not per-key) rate limiting plus a per-user active-key cap,
so minting keys can't multiply the daily quota. Name-length cap;
race-safe migration and usage recording.

Keys are strictly personal: one owner, full access, no sharing.
Team-shared / scoped keys and per-key access levels were intentionally
left out of this PR to keep it small and easy to review; they can follow
as a separate, focused change.

> Note: the screenshots from the original revision showed an earlier
team-scoped design and need refreshing.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have run `task check` to verify linters, typechecks, and tests
pass
- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.

---------

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-07-23 07:52:57 +00:00

87 lines
4.0 KiB
Gherkin

@jwt @auth @apikey
Feature: API Keys management API
Tests for the portal API-keys REST API, which lets a user mint, list, and
revoke named personal API keys.
Endpoints (all @EnterpriseEndpoint, JWT/ROLE required):
- GET /api/v1/proprietary/ui-data/infrastructure/api-keys (list)
- POST /api/v1/proprietary/ui-data/infrastructure/api-keys (create)
- DELETE /api/v1/proprietary/ui-data/infrastructure/api-keys/{id} (revoke)
Because these are @EnterpriseEndpoint, authenticated responses may be 200
(enterprise enabled) or 403 (feature not in this build). Unauthenticated
requests must always be rejected with 401.
The legacy single per-user key (the global API key) must keep working so no
key created before multi-key support is ever lost.
Admin credentials: username=admin, password=stirling
Global API key: 123456789
# =========================================================================
# LIST
# =========================================================================
@positive
Scenario: Admin can list API keys
Given I am logged in as admin
When I send a GET request to "/api/v1/proprietary/ui-data/infrastructure/api-keys" with JWT authentication
Then the response status code should be one of "200, 403"
@negative
Scenario: Unauthenticated list request returns 401
When I send a GET request to "/api/v1/proprietary/ui-data/infrastructure/api-keys" with no authentication
Then the response status code should be 401
# =========================================================================
# CREATE
# =========================================================================
@positive
Scenario: Admin can create a personal API key
Given I am logged in as admin
When I send a JSON POST request to "/api/v1/proprietary/ui-data/infrastructure/api-keys" with JWT authentication and body '{"name": "bdd_personal_key"}'
Then the response status code should be one of "200, 403"
@negative
Scenario: Creating a key without a name is rejected
Given I am logged in as admin
When I send a JSON POST request to "/api/v1/proprietary/ui-data/infrastructure/api-keys" with JWT authentication and body '{"name": ""}'
Then the response status code should be one of "400, 403"
@negative
Scenario: Unauthenticated create request returns 401
When I send a POST request to "/api/v1/proprietary/ui-data/infrastructure/api-keys" with no authentication
Then the response status code should be 401
# =========================================================================
# REVOKE
# =========================================================================
@negative
Scenario: Unauthenticated revoke request returns 401
When I send a DELETE request to "/api/v1/proprietary/ui-data/infrastructure/api-keys/1" with no authentication
Then the response status code should be 401
@positive
Scenario: Admin revoking a non-existent key is handled, not a bypass
Given I am logged in as admin
When I send a DELETE request to "/api/v1/proprietary/ui-data/infrastructure/api-keys/999999" with JWT authentication
Then the response status code should be one of "204, 403, 404"
# =========================================================================
# LEGACY KEY BACKWARD COMPATIBILITY
# =========================================================================
@positive
Scenario: The legacy global API key still authenticates
When I send a GET request to "/api/v1/auth/me" with API key "123456789"
Then the response status code should be 200
And the response JSON should have field "username"
@negative
Scenario: An unknown API key is rejected
When I send a GET request to "/api/v1/auth/me" with API key "not-a-real-api-key-000"
Then the response status code should be 401