Skip to content

feat(api): add API-provisionable service accounts#9399

Open
clwluvw wants to merge 4 commits into
makeplane:previewfrom
clwluvw:svc-account
Open

feat(api): add API-provisionable service accounts#9399
clwluvw wants to merge 4 commits into
makeplane:previewfrom
clwluvw:svc-account

Conversation

@clwluvw

@clwluvw clwluvw commented Jul 11, 2026

Copy link
Copy Markdown

Description

Add machine/service accounts so a self-hosted workspace can be provisioned entirely over the API: a distinct, active actor with an API-mintable token and no invite / email-verification / password flow.

What each account is (created in one transaction): an active, email-verified User flagged as a bot (is_bot=True, bot_type=SERVICE) with an unusable password and a synthetic unique username/email; a WorkspaceMember at the requested role; and a workspace-scoped bot APIToken (user_type=Bot, is_service=True). is_bot blocks interactive login (BOT_USER_LOGIN_FORBIDDEN); the account acts only via its token, and its writes attribute to it via created_by.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Summary by CodeRabbit

  • New Features
    • Added workspace service-account provisioning via an admin-only HTTP API and a management command.
    • Introduced full service-account token lifecycle: list, mint, rotate, revoke, and decommission.
    • Added support for service-account roles (admin, member, guest) with configurable username and display name behavior.
  • Security
    • Service-account API tokens are never exposed in logs or listing responses; minted/rotated plaintext is shown only once.
    • Enforced workspace-admin-only access for token and service-account management actions.
  • Documentation
    • Added end-user documentation for service-account constraints, identity behavior, token management, and decommissioning semantics.
  • Tests
    • Added contract and unit tests covering provisioning flows, token lifecycle edge cases, uniqueness handling, and log redaction.

Copilot AI review requested due to automatic review settings July 11, 2026 22:51
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Service-account support adds transactional workspace-scoped bot provisioning through a management command and admin API, token minting, rotation, revocation, and decommissioning, with one-time token responses, activity-log redaction, authorization checks, tests, and documentation.

Changes

Service Account Lifecycle

Layer / File(s) Summary
Provisioning core
apps/api/plane/db/models/user.py, apps/api/plane/utils/service_account.py, apps/api/plane/tests/unit/utils/test_service_account.py
Defines service-account roles and service-bot identities, and implements transactional provisioning, token lifecycle operations, decommissioning, and role-resolution tests.
Admin API lifecycle
apps/api/plane/api/serializers/..., apps/api/plane/api/views/..., apps/api/plane/api/urls/...
Adds workspace-owner endpoints and serializers for creation, token listing, minting, rotation, revocation, and service-account deletion.
Management command flow
apps/api/plane/db/management/commands/create_service_account.py
Adds CLI identity options, uniqueness validation, integrity-error handling, and one-time token output.
Secret redaction and validation
apps/api/plane/middleware/logger.py, apps/api/plane/tests/..., docs/service-accounts.md
Adds response-body redaction, lifecycle and authorization contract tests, middleware unit tests, and service-account documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant ServiceAccountAPIEndpoint
  participant create_service_account
  participant APIToken
  participant APITokenLogMiddleware
  Admin->>ServiceAccountAPIEndpoint: POST service-account request
  ServiceAccountAPIEndpoint->>create_service_account: validate and provision account
  create_service_account->>APIToken: mint workspace-scoped token
  create_service_account-->>ServiceAccountAPIEndpoint: account and plaintext token
  ServiceAccountAPIEndpoint->>APITokenLogMiddleware: mark response for redaction
  ServiceAccountAPIEndpoint-->>Admin: HTTP 201 with one-time token
  APITokenLogMiddleware-->>APITokenLogMiddleware: persist "[REDACTED]" response body
Loading

Suggested reviewers: dheeru0198

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding API-provisionable service accounts.
Description check ✅ Passed The description covers the feature and type of change, but it omits the template's Test Scenarios and References sections.
Docstring Coverage ✅ Passed Docstring coverage is 95.24% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class, API-provisionable “service accounts” to enable fully automated workspace provisioning: a bot user + workspace membership + workspace-scoped API token created atomically, with safeguards to prevent interactive login and to avoid persisting minted tokens in API activity logs.

Changes:

  • Introduces shared service-account provisioning utility used by both a new management command and a new admin-scoped HTTP endpoint.
  • Adds response-body redaction support to the API token logging middleware to prevent logging newly minted credentials.
  • Adds contract/unit tests plus documentation covering service-account behavior and surfaces.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/service-accounts.md Documents service accounts, roles, command usage, and the admin endpoint (including token handling/logging notes).
apps/api/plane/utils/service_account.py Core transactional provisioning logic for bot user + membership + token.
apps/api/plane/tests/unit/middleware/test_logger.py Adds tests proving response bodies are logged by default and redacted when flagged.
apps/api/plane/tests/contract/api/test_service_account.py Contract tests for both the management command and the HTTP endpoint, including attribution and log redaction.
apps/api/plane/middleware/logger.py Adds redact_response_body() + attribute flagging and middleware support to redact logged response bodies.
apps/api/plane/db/models/user.py Extends BotTypeEnum with SERVICE bot type.
apps/api/plane/db/management/commands/create_service_account.py New management command to create a service account and print the minted token once.
apps/api/plane/api/views/service_account.py New admin-only endpoint to create service accounts and return the token once (with log redaction).
apps/api/plane/api/views/init.py Exports the new service account API view.
apps/api/plane/api/urls/service_account.py Adds route for POST /api/v1/workspaces/{slug}/service-accounts/.
apps/api/plane/api/urls/init.py Includes service-account URL patterns in the API router.
apps/api/plane/api/serializers/service_account.py Adds request/response serializers for service-account provisioning.
apps/api/plane/api/serializers/init.py Exports the new service-account serializers.

Comment thread apps/api/plane/utils/service_account.py
Comment thread docs/service-accounts.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/db/management/commands/create_service_account.py`:
- Around line 42-57: Wrap the create_service_account call in handle with an
IntegrityError handler and re-raise a readable CommandError for email uniqueness
failures, covering both the user-supplied email race and synthetic uuid4 email
collisions. Preserve the existing validation and successful creation behavior.

In `@apps/api/plane/utils/service_account.py`:
- Around line 56-64: Update resolve_service_account_role so integer inputs are
validated against the allowed values in SERVICE_ACCOUNT_ROLES before returning;
preserve valid integers and raise the same ValueError style used for invalid
string roles when an integer is unsupported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a2e8f2d-d3dc-4072-b115-11c1dbf1cd76

📥 Commits

Reviewing files that changed from the base of the PR and between dc9d80b and 3227e94.

📒 Files selected for processing (13)
  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/service_account.py
  • apps/api/plane/api/urls/__init__.py
  • apps/api/plane/api/urls/service_account.py
  • apps/api/plane/api/views/__init__.py
  • apps/api/plane/api/views/service_account.py
  • apps/api/plane/db/management/commands/create_service_account.py
  • apps/api/plane/db/models/user.py
  • apps/api/plane/middleware/logger.py
  • apps/api/plane/tests/contract/api/test_service_account.py
  • apps/api/plane/tests/unit/middleware/test_logger.py
  • apps/api/plane/utils/service_account.py
  • docs/service-accounts.md

Comment thread apps/api/plane/db/management/commands/create_service_account.py Outdated
Comment thread apps/api/plane/utils/service_account.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
docs/service-accounts.md (1)

67-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Specify a language for fenced code blocks.

The code blocks at lines 67 and 89 lack a language specifier, triggering markdownlint MD040. Use text for the command output block and http for the HTTP request example.

📝 Proposed fix
-```
+```text
 Service account created successfully
   user_id  : 1c2f...c7f3

And at line 89:

-```
+```http
 POST /api/v1/workspaces/{slug}/service-accounts/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/service-accounts.md` at line 67, Update the two fenced code blocks in
the service-accounts documentation: add the text language specifier to the
command output block and the http language specifier to the HTTP request
example, without changing their contents.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docs/service-accounts.md`:
- Line 67: Update the two fenced code blocks in the service-accounts
documentation: add the text language specifier to the command output block and
the http language specifier to the HTTP request example, without changing their
contents.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0439c2a-aad6-4ff1-a43e-621418c93b39

📥 Commits

Reviewing files that changed from the base of the PR and between 3227e94 and 6137f99.

📒 Files selected for processing (14)
  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/service_account.py
  • apps/api/plane/api/urls/__init__.py
  • apps/api/plane/api/urls/service_account.py
  • apps/api/plane/api/views/__init__.py
  • apps/api/plane/api/views/service_account.py
  • apps/api/plane/db/management/commands/create_service_account.py
  • apps/api/plane/db/models/user.py
  • apps/api/plane/middleware/logger.py
  • apps/api/plane/tests/contract/api/test_service_account.py
  • apps/api/plane/tests/unit/middleware/test_logger.py
  • apps/api/plane/tests/unit/utils/test_service_account.py
  • apps/api/plane/utils/service_account.py
  • docs/service-accounts.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/api/plane/api/serializers/init.py
  • apps/api/plane/api/urls/init.py
  • apps/api/plane/api/views/init.py
  • apps/api/plane/api/urls/service_account.py
  • apps/api/plane/db/management/commands/create_service_account.py
  • apps/api/plane/api/serializers/service_account.py
  • apps/api/plane/api/views/service_account.py
  • apps/api/plane/tests/contract/api/test_service_account.py
  • apps/api/plane/middleware/logger.py
  • apps/api/plane/db/models/user.py
  • apps/api/plane/utils/service_account.py

Copilot AI review requested due to automatic review settings July 23, 2026 20:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

apps/api/plane/api/views/service_account.py:309

  • The revoke endpoint will delete any token for this user/workspace, including non-service tokens (is_service=False). To keep the lifecycle endpoints scoped to service-account tokens, consider filtering by is_service/user_type as well.
        token = APIToken.objects.filter(id=token_id, user_id=user_id, workspace=member.workspace).first()
        if token is None:
            return Response(status=status.HTTP_404_NOT_FOUND)

apps/api/plane/api/views/service_account.py:344

  • The rotate endpoint can currently rotate any token for this user/workspace (including non-service tokens), and it will also rotate an already-inactive token—allowing repeated calls to mint unlimited replacements from the same old token. Consider scoping rotation to active service tokens only.
        token = APIToken.objects.filter(id=token_id, user_id=user_id, workspace=member.workspace).first()
        if token is None:
            return Response(status=status.HTTP_404_NOT_FOUND)

Comment thread apps/api/plane/db/management/commands/create_service_account.py
Comment thread apps/api/plane/api/views/service_account.py Outdated
Comment thread apps/api/plane/api/views/service_account.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/utils/service_account.py`:
- Around line 157-195: Update rotate_service_account_token so the replacement
inherits token.expired_at when expired_at is omitted, while preserving an
explicitly supplied new expiry. Pass the resolved expiry to
mint_service_account_token and leave the existing deactivation behavior
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c55ac6d8-1e81-4375-82ea-09c52a8efbd0

📥 Commits

Reviewing files that changed from the base of the PR and between 3fed241 and 64d6fdc.

📒 Files selected for processing (9)
  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/service_account.py
  • apps/api/plane/api/urls/service_account.py
  • apps/api/plane/api/views/__init__.py
  • apps/api/plane/api/views/service_account.py
  • apps/api/plane/db/management/commands/create_service_account.py
  • apps/api/plane/tests/contract/api/test_service_account.py
  • apps/api/plane/utils/service_account.py
  • docs/service-accounts.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/service-accounts.md

Comment thread apps/api/plane/utils/service_account.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/api/plane/tests/contract/api/test_service_account.py (1)

517-534: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for minting without a label.

Every mint test supplies label, so the omitted-label path (data.get("label") → None) is never exercised — the same gap flagged on apps/api/plane/api/views/service_account.py. Add a case posting {} and asserting a 201 with a usable label.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/tests/contract/api/test_service_account.py` around lines 517 -
534, The service-account token mint tests do not cover requests without a label.
Add a test alongside test_mint_token_returns_value_once_and_authenticates that
posts an empty JSON payload to the token endpoint, asserts HTTP 201, and
verifies the response contains a usable non-empty label and token.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/api/plane/tests/contract/api/test_service_account.py`:
- Around line 517-534: The service-account token mint tests do not cover
requests without a label. Add a test alongside
test_mint_token_returns_value_once_and_authenticates that posts an empty JSON
payload to the token endpoint, asserts HTTP 201, and verifies the response
contains a usable non-empty label and token.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d738ed7e-fad3-4c4b-86ac-5a5c8ec84081

📥 Commits

Reviewing files that changed from the base of the PR and between 64d6fdc and 0740a45.

📒 Files selected for processing (13)
  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/service_account.py
  • apps/api/plane/api/urls/__init__.py
  • apps/api/plane/api/urls/service_account.py
  • apps/api/plane/api/views/__init__.py
  • apps/api/plane/api/views/service_account.py
  • apps/api/plane/db/management/commands/create_service_account.py
  • apps/api/plane/db/models/user.py
  • apps/api/plane/middleware/logger.py
  • apps/api/plane/tests/contract/api/test_service_account.py
  • apps/api/plane/tests/unit/middleware/test_logger.py
  • apps/api/plane/tests/unit/utils/test_service_account.py
  • apps/api/plane/utils/service_account.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/api/plane/api/serializers/init.py
  • apps/api/plane/api/views/init.py
  • apps/api/plane/tests/unit/utils/test_service_account.py
  • apps/api/plane/db/models/user.py
  • apps/api/plane/api/urls/init.py
  • apps/api/plane/api/urls/service_account.py
  • apps/api/plane/middleware/logger.py
  • apps/api/plane/utils/service_account.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread apps/api/plane/utils/service_account.py
Copilot AI review requested due to automatic review settings July 25, 2026 19:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Comment thread apps/api/plane/utils/service_account.py Outdated
Comment thread apps/api/plane/db/management/commands/create_service_account.py Outdated
Comment thread apps/api/plane/api/views/service_account.py
Comment thread docs/service-accounts.md Outdated
Copilot AI review requested due to automatic review settings July 25, 2026 19:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread docs/service-accounts.md Outdated
Copilot AI review requested due to automatic review settings July 25, 2026 19:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment thread docs/service-accounts.md Outdated
Comment thread apps/api/plane/utils/service_account.py Outdated
Copilot AI review requested due to automatic review settings July 25, 2026 21:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread apps/api/plane/utils/service_account.py Outdated
clwluvw added 4 commits July 25, 2026 23:29
Add machine/service accounts so a self-hosted workspace can be provisioned
entirely over the API: a distinct, active actor with an API-mintable token and
no invite / email-verification / password flow.

What each account is (created in one transaction): an active, email-verified
User flagged as a bot (is_bot=True, bot_type=SERVICE) with an unusable password
and a synthetic unique username/email; a WorkspaceMember at the requested role;
and a workspace-scoped bot APIToken (user_type=Bot, is_service=True). is_bot
blocks interactive login (BOT_USER_LOGIN_FORBIDDEN); the account acts only via
its token, and its writes attribute to it via created_by.

- create_service_account management command (--workspace --name --role
  [--email --description]) that prints the token.
- Optional admin-scoped POST /api/v1/workspaces/{slug}/service-accounts/
  (WorkspaceOwnerPermission), mirroring the command over HTTP.
- Shared plane/utils/service_account.py helper so command and endpoint cannot
  drift.
- Add SERVICE to BotTypeEnum. bot_type is a choice-less CharField, so no
  migration is required.
- Security: redact secret-bearing response bodies from api_activity_logs so the
  minted token is never persisted in plaintext (response-body analogue of the
  existing SENSITIVE_HEADERS redaction).
- docs/service-accounts.md and contract + unit tests (token authenticates as a
  distinct actor, writes attribute to it, non-admin gets 403, token not logged).

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
… accounts

Let an external provisioner assign stable, readable identity to a service
account instead of the server-generated defaults, so accounts can be created
idempotently (looked up by a known username) and show a readable name in the
workspace members UI.

- POST /api/v1/workspaces/{slug}/service-accounts/ and the create_service_account
  command accept optional username and display_name (--username/--display-name).
- username is globally unique and bounded only by length (max 128) — matching
  regular Plane usernames, which have no charset validator (a random uuid handle).
  A collision returns 409 {"code": "USERNAME_ALREADY_EXISTS"} (or a CommandError),
  never a silently mutated name. A race past the pre-check is caught and mapped the
  same way; any other IntegrityError is re-raised rather than mislabeled.
- display_name lands on User.display_name; both fields fall back to synthetic /
  name when omitted, and a blank value normalizes to the same fallback on both
  paths. The synthetic email is always a fresh unique svc_<uuid> address.
- Response echoes the effective username + display_name.
- Docs + contract/unit tests: custom identity, collision, faithful race, genuine
  atomic rollback, blank normalization, and command flag round-trip.

Token semantics are unchanged (token lifecycle is a separate change).

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
Add the lifecycle operations an external reconcile/rotation loop needs on top of
the atomic create. All are admin-scoped (WorkspaceOwnerPermission), follow
public-API conventions (APIKeyAuthentication, cursor pagination, OpenAPI), and
are scoped to a SERVICE bot member of the workspace.

- GET  /workspaces/{slug}/service-accounts/{user_id}/tokens/ — list tokens with
  metadata (label, timestamps, expiry, is_active); the secret value is always
  withheld.
- POST .../tokens/ — mint an additional workspace-scoped bot token (value once,
  optional expired_at).
- POST .../tokens/{token_id}/rotate/ — atomically mint a replacement (value once)
  and deactivate the old token, so the old value stops authenticating.
- DELETE .../tokens/{token_id}/ — revoke (soft-delete) a token.
- DELETE /workspaces/{slug}/service-accounts/{user_id}/ — decommission:
  deactivate all tokens, remove ProjectMember + WorkspaceMember rows, deactivate
  the User (row kept so created_by attribution survives). Hard-guarded to
  is_bot + bot_type=SERVICE; a human or other bot returns 400.

- Mint/rotate responses carry the plaintext token, so their bodies are redacted
  from api_activity_logs (reuses redact_response_body from the create endpoint).
- Resolving the account intentionally does not filter membership is_active, so a
  deactivated (but not decommissioned) account whose tokens still authenticate
  can still be listed, revoked, and decommissioned.
- Shared mint/rotate/decommission helpers in plane/utils/service_account.py.

Tests cover: list masks values, mint/rotate return + authenticate, rotate/revoke
invalidate the old token, decommission cascade + attribution survival, the
is_bot/SERVICE guard (human + other bot -> 400), cross-workspace isolation,
non-admin denial, and mint/rotate log redaction. A module autouse fixture resets
the API-key throttle cache per test (otherwise the throttle counter is shared and
uncleared across tests). Docs updated.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
Rotation expiry semantics (CodeRabbit):
- Rotating a token now INHERITS the source token's expired_at when the caller
  omits it, instead of minting a never-expiring replacement. An explicit null
  still clears the expiry; a supplied timestamp must be in the future.
- The rotate body uses an OmittableDateTimeField so an empty form/multipart
  value reads as 'omitted' (inherit), not as null — closing a hole where a
  form-encoded 'expired_at=' produced an immortal token.
- Rotation is restricted to ACTIVE tokens via a race-safe conditional UPDATE
  (409 TOKEN_NOT_ACTIVE); inheriting an already-elapsed expiry is refused
  (400 SOURCE_TOKEN_EXPIRY_ELAPSED). Mint and rotate reject a past expired_at.
- The deactivation UPDATE writes updated_by unconditionally (None when there is
  no actor), matching BaseModel.save().
- Token-rotation exceptions carry code + status_code and are mapped in one place.

Copilot: revoke/rotate are already scoped by construction (user_id is a SERVICE
bot member of the workspace, tokens are workspace-scoped is_service bot tokens);
the one real gap (rotating an inactive token) is closed above. is_service is
deliberately NOT added to the revoke lookup — it would make a legacy non-service
token unrevokable (fail-open).

Docstring Coverage gate: document every callable in the new files (handlers,
command, helpers, and all contract/unit tests).

Docs: correct the rotate section (remaining-lifetime inheritance, the three
expiry cases, 409 vs 404, future-timestamp requirement).

Tests: rotate expiry inheritance / explicit-null / explicit-future / past
rejected / form-encoded-empty-inherits / inactive-source-409 / elapsed-source-400.

- Cover minting a token with no label (generated default) — CodeRabbit.

- Do not conflate an omitted vs blank description: `create_service_account`
  now defaults only when description is None, so an explicit "" is preserved
  (serializer/CLI/view thread None as the "omitted" sentinel) — Copilot.

- Scope decommission token deactivation to the given workspace (tokens are
  workspace-scoped) so retiring an account in one workspace never touches its
  tokens elsewhere — Copilot.
- Normalize whitespace-only --email/--username/--display-name in the command
  (strip; blank -> None) so " " cannot create an all-whitespace identity — Copilot.
- Document 401/403/404 on the create endpoint's OpenAPI schema — Copilot.
- Docs: use a future expired_at in the mint example so it stays valid — Copilot.

- Scope the token list/revoke/rotate queries to is_service tokens so the
  service-account token API only manages service tokens — Copilot.
- Validate identity-field max lengths in the command (mirroring the serializer)
  so an over-long value fails with CommandError, not a raw DB DataError — Copilot.
- resolve_service_account_role: raise the role ValueError `from None` so the
  user-facing message isn't buried under a KeyError chain — Copilot.
- Docs: phrase the synthetic email as a default (--email can override it) and
  clarify that bots are hidden from the web-app member list but listed by the
  public members API — Copilot.

- decommission token deactivation now also sets updated_at/updated_by (like
  rotate) via a shared _audit_fields() helper: QuerySet.update() bypasses
  save(), so without it a decommissioned token kept a stale updated_at and no
  actor attribution. Tests assert both the rotate and decommission paths stamp
  updated_at and attribute the deactivation to the acting admin — Copilot.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
Copilot AI review requested due to automatic review settings July 25, 2026 21:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

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.

2 participants