Skip to content

[feature]: Scoped API tokens (read-only / no delete) and a per-token rate limit #10221

Open

Description

@pitupi

Is there an existing issue for this?

Searched on 2026-11-07 for api token scope / read-only token / api key permissions: no match (closest: #9155, an AI-assistant proposal that does not cover token scopes).

Summary

An API key can do everything its user can do through the API, including DELETE of work items, comments and projects. The APIToken model (apps/api/plane/db/models/api.py) has label, is_active, expired_at, user_type but no scope. The rate limit is a single global API_KEY_RATE_LIMIT (default 60/minute) applied per key, with no per-token override. Please add:

  1. Scopes on the token: at least read (GET only), write (GET/POST/PATCH) and delete as separate grants; default stays "everything" for existing tokens so nothing breaks.
  2. Optional per-token rate limit (nullable column; empty = use API_KEY_RATE_LIMIT).

Why should this be worked on?

Bots and AI agents get their own key so that people can audit what they did. Today a bot that only needs to read and comment can still wipe a project with one call. Reproduced on v1.4.3: with the same key that lists and comments, DELETE /issues/{id}/ returns 204 and DELETE /projects/{id}/ returns 204 (repro_limitacoes.py, section 4, uses a throwaway project). Operators who run several bots also cannot give a read-heavy sync job more requests/minute than a bot that writes once a day, without raising the limit for everyone.

Proposed behaviour

  • Token creation (UI and API) accepts scopes (list). Existing rows migrate to all scopes.
  • APIKeyAuthentication attaches the scopes to the request; a small permission class checks the HTTP method (GET/HEAD/OPTIONS → read; POST/PUT/PATCH → write; DELETE → delete) and answers 403 with the missing scope.
  • ApiKeyRateThrottle reads the token's own limit when set, otherwise settings.API_KEY_RATE_LIMIT.

Implementation sketch (not tested against the Plane test suite)

Pinned to 618d0eb7.

  • db/models/api.py: scopes = ArrayField(models.CharField(max_length=16), default=default_scopes) and rate_limit = models.CharField(max_length=32, null=True, blank=True) + migration with the default ["read","write","delete"].
  • api/middleware/api_authentication.py (APIKeyAuthentication.authenticate): return the token as auth, so permission classes can read request.auth.scopes.
  • api/rate_limit.py: rate = settings.API_KEY_RATE_LIMIT is a class attribute; override get_rate() to look at the token when it is available.
  • A TokenScopePermission listed in DEFAULT_PERMISSION_CLASSES for the /api/v1 views.

Tests worth adding

  1. Token with scopes=["read"]: GET 200, POST/PATCH/DELETE 403.
  2. Token with ["read","write"]: PATCH 200, DELETE 403.
  3. Legacy token (migrated): everything still works.
  4. Token with rate_limit="5/minute": the sixth call in a minute returns 429; another token is unaffected.

Workaround we use meanwhile

The CLI used by our agents does not expose delete, and the rules ask agents never to call the API directly to delete. That is a convention, not a barrier. For the rate limit, the operator raises API_KEY_RATE_LIMIT for the whole instance.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions