Repository navigation
Description
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:
- Scopes on the token: at least
read(GET only),write(GET/POST/PATCH) anddeleteas separate grants; default stays "everything" for existing tokens so nothing breaks. - 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. APIKeyAuthenticationattaches the scopes to the request; a small permission class checks the HTTP method (GET/HEAD/OPTIONS→ read;POST/PUT/PATCH→ write;DELETE→ delete) and answers403with the missing scope.ApiKeyRateThrottlereads the token's own limit when set, otherwisesettings.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)andrate_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 asauth, so permission classes can readrequest.auth.scopes.api/rate_limit.py:rate = settings.API_KEY_RATE_LIMITis a class attribute; overrideget_rate()to look at the token when it is available.- A
TokenScopePermissionlisted inDEFAULT_PERMISSION_CLASSESfor the/api/v1views.
Tests worth adding
- Token with
scopes=["read"]: GET200, POST/PATCH/DELETE403. - Token with
["read","write"]: PATCH200, DELETE403. - Legacy token (migrated): everything still works.
- Token with
rate_limit="5/minute": the sixth call in a minute returns429; 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