User License Cleanup & Dashboard for Bitbucket
Bitbucket v1.0.0 · Release candidate
Finds inactive Bitbucket seats, explains exactly which grant keeps each one billable, and refuses to bulk-action anything that looks like CI.
Overview
Most user-cleanup apps stop at “here is a list of people who have not logged in — tick the boxes to deactivate them”. That list is the easy half of the problem, and acting on it blindly is how you break a production pipeline.
Billable-path clarity. For every user the app shows which access path keeps the seat billable — a direct repository grant, a project grant, an inferred group grant, or plain workspace membership — and, critically, whether the app can remove it or whether you have to do something in Atlassian admin. A seat only frees up when every path is gone, so a list of names without paths tells you nothing actionable.
Bitbucket-specific service-account safety. In Bitbucket, a large share of the busiest accounts are CI identities authenticating with app passwords or API tokens. Those never register as interactive product activity, so by the standard “last login” measure they look completely dead. This app subscribes to push and pull-request events so it can tell “nobody is using this seat” apart from “a robot is using this seat hard”.
Highlights
Six tabs, one honest story
Overview (including which data sources actually worked), Users with full billable-path drill-down, Excluded showing which rule or heuristic spared each user, Cleanup with a Safe/Live toggle and typed confirmation, Runs & audit, and Settings.
Safe mode produces the same plan object Live executes
There is no separate code path. Read the plan, then read the Skipped by safety rails list underneath it — that list is the app telling you what it refuses to touch, and why.
Git activity from events, not history
Push and pull-request product events cost nothing and are recorded from installation onwards. No commit or pull-request history is ever read — that approach cost two API calls per repository per run and could not complete inside Bitbucket’s hourly rate limit on a large workspace.
Scans that do not scale with repository count
Inactivity comes from the workspace permissions endpoint, which paginates by user — one call per hundred members whether the workspace holds ten repositories or a hundred thousand. Repository detail is a background index a scan never waits on.
Exact restore
Every applied action records the permission level it removed, so “Restore all applied changes” is an exact replay. The app never deletes an account and never performs an irreversible operation.
Quarantine, not silent exclusion
A likely service account is quarantined from bulk actions rather than excluded silently, because a wrong auto-exclusion is as bad as a wrong revocation. Confirm ownership, then add an explicit exclusion — explicit exclusions survive threshold changes; heuristics do not.
How to use it without breaking CI/CD
Bitbucket is not Jira. Revoking the wrong seat here does not inconvenience someone — it fails builds, blocks deployments and can take a release train down. The intended order:
1. Never run Live before a Safe-mode pass. Read the plan, then read the skipped-by-safety-rails list underneath it.
2. Treat every “likely service account” as a real finding. Confirm ownership, then add an explicit exclusion.
3. Give the git-activity signal time to accumulate. On a fresh install that signal is empty, so lean on the quarantine list rather than on the safety window.
4. Keep the safety window at 30 days or more. Any push or PR inside the window makes a user un-flaggable regardless of login history. A nightly job that only runs monthly needs a window longer than its cadence.
5. Leave scheduled scans on “report only”. Auto-apply exists, is opt-in, and still honours every rail — but nobody reviews the plan first.
6. Know the restore path before you need it. Use it the moment a build starts failing; investigate afterwards.
User guide
Who this is forBitbucket workspace administrators. The organization API key that unlocks inactivity detection has to be created by an organization admin, who may be someone else.
Bitbucket is not Jira. Revoking the wrong seat here does not inconvenience someone — it fails builds, blocks deployments and can take a release train down. The setup order below exists for that reason, and step three is the one that protects your pipelines.
| Surface | Where to find it |
|---|---|
| The dashboard | Workspace settings → Forge Apps → User License Cleanup |
| The organization API key | The Settings tab. Create the key at admin.atlassian.com → Settings → API keys as an organization admin |
| Exclusions | The Excluded tab |
| Plans, apply and restore | The Cleanup tab |
-
Create the organization API key
Last-login data does not come from Bitbucket’s API. It comes from the Atlassian Admin (Organizations) API, which uses a credential Forge cannot mint for itself.
At admin.atlassian.com → Settings → API keys → Create API key, give it a name and an expiry, then copy the key and the organization id — the key is shown once.
-
Store it in the app
Workspace settings → Forge Apps → User License Cleanup → Settings. Paste the key (the org id is optional — leave it blank to auto-detect) and choose Verify and save.
The key is validated against the API before it is stored, is written with Forge’s encrypted secret store, is never returned to the browser, and is only ever sent to
api.atlassian.com. Only the last four characters are shown back to you, so you can confirm which key is installed.The key is optional to install but required for inactivity detection. Without it the app still maps billable paths and flags service accounts; it just cannot tell you who is idle.
-
Configure exclusions before your first live run
This is the step that protects your pipelines. On the Excluded tab, add a rule for every CI, bot and integration account you know about.
Prefer a user exclusion (exact account id) or a pattern exclusion for anything you cannot afford to break. Explicit exclusions survive threshold changes; heuristics do not.
-
Run a scan and read the data-source notes first
Run scan now on the toolbar. The scan runs in the background and results appear as batches complete.
Read the Overview tab’s honest report on which data sources actually worked before trusting any number on the page.
-
Do a Safe-mode pass, then read what was skipped
Safe mode produces the exact same plan object that Live mode executes — there is no separate code path. Read the plan, then read the Skipped by safety rails list underneath it. That list is the app telling you what it refuses to touch, and why.
Never run Live mode before a Safe-mode pass.
-
Treat every “likely service account” as a real finding
The heuristic quarantines them from bulk actions rather than excluding them silently, because a wrong auto-exclusion is as bad as a wrong revocation. Confirm ownership for each, then add an explicit exclusion.
-
Apply, and know the restore path before you need it
Live apply requires a typed confirmation. Every applied action records the permission level it removed, so Restore all applied changes on the Cleanup tab is an exact replay.
Use it the moment a build starts failing; investigate afterwards. The app never deletes an account and never performs an irreversible operation — the worst case is a revoked permission grant, one button away from being restored.
Good to know
- Give the git-activity signal time to accumulate before your first cleanup. Push and pull-request events are the only signal separating a token-driven CI account from an abandoned human account, and they are collected from installation onwards — no history is read. On a fresh install that signal is empty, so lean on the quarantine list rather than on the safety window. Do not act on the first scan of a fresh install.
- Keep the safety window at 30 days or more. Any push or PR inside the window makes a user un-flaggable regardless of login history. A nightly job that only runs monthly needs a window longer than its cadence.
- Leave scheduled scans on “report only”. Auto-apply exists, is opt-in, and still honours every rail — but nobody reviews the plan first, and a pipeline identity that starts being used between scans will already have been revoked.
- The six tabs: Overview (summary, cost impact, inactivity trend, where waste concentrates, and which data sources worked), Users (drill-down with the full billable-path breakdown), Excluded (every rule, plus every user spared and which rule or heuristic spared them), Cleanup (Safe/Live, plan preview, bulk apply, restore), Runs & audit, Settings.
- Defaults, and why: inactivity threshold 90 days (survives parental leave and sabbaticals; 30 days produces too many false positives in engineering orgs), never-logged-in grace 14 days (a pending invite is not waste), recent-activity safety window 30 days, service-account confidence threshold 50%, notification lead time 7 days, weekly report-only scans (licence waste changes slowly; daily scans burn invocations for no benefit).
- CSV export is copy-paste, not a download. Forge UI Kit runs sandboxed with no filesystem or Blob access, so the CSV is rendered in a modal with the suggested filename above it. The content is byte-identical to what a download would produce.
- Notifications are recorded, not emailed — Forge has no first-party outbound email API. Enabling them still changes behaviour end to end: the first plan for a flagged user records a notice with a deadline instead of revoking anything, and every later plan refuses to touch that user until the lead time elapses. You send the message, then use They replied “still active” in the user detail modal to block the pending cleanup.
If something looks wrong
A user shows “No data” and is never auto-actioned.
The Admin API only covers managed accounts. Users outside your verified domains — external collaborators, contractors on personal accounts — return no record, are classified unknown, and are never auto-actioned.
Some billable paths are missing from a user’s breakdown.
The repository access index is eventually consistent. It builds in the background in bounded batches and parks itself when the workspace nears its hourly API limit, resuming on the next daily trigger. Inactivity results are unaffected, and the Overview tab states where the index has got to.
A path is labelled “Group grant (inferred)” and cannot be removed.
Bitbucket exposes user groups only through the deprecated 1.0 API, which Forge apps cannot call. Group-derived paths are labelled, marked non-removable, and carry instructions pointing at workspace settings. For a hard guarantee, use a user or pattern exclusion rather than a group one.
A project-grant revocation failed with 403.
Expected, and deliberate. Project-grant revocation may additionally require admin:project:bitbucket, which also confers project deletion — a scope a licence-cleanup app has no business holding, so it is not requested. The action is marked failed with the API’s message and written to the audit log; it fails loudly, not silently.
A workspace membership path cannot be removed.
Bitbucket does not support it via the API — membership is managed in Atlassian admin or via SCIM. The app reports these paths and tells you where to go.
What it does not do
- Delete an account, or perform any irreversible operation. The worst case is a revoked permission grant.
- Read commit or pull-request history. Inactivity comes from the workspace permissions endpoint, which paginates by user; git activity comes from product events, which cost nothing.
- Remove a user from a group, or remove workspace membership — neither is exposed to Forge apps.
- Auto-action an account it has no data for.
- Run on Data Center. This is a Forge Cloud app.
Permissions & scopes
| Scope | Why it's needed |
|---|---|
storage:app | Settings, exclusions, scans, plans and the audit log |
read:workspace:bitbucket | Workspace membership and workspace-level permission grants |
read:project:bitbucket | Project listing and project-level permission grants |
read:repository:bitbucket | Repository listing and repository permission grants, for the background access index. No commit history is read. |
read:pullrequest:bitbucket | Required to receive pull-request events. Not used for any REST call; the app never reads pull request history. |
read:permission:bitbucket, write:permission:bitbucket, delete:permission:bitbucket | Reading and revoking explicit user permission grants — the only cleanup actions this app can perform natively |
admin:repository:bitbucket | Required by PUT/DELETE on the repository permissions-config endpoint. The scope docs note that admin:repository alone does not allow modification of repository permissions, which is why the write and delete permission scopes are still needed alongside it. |
admin:project:bitbucket is deliberately not requested, even though project-grant revocation may require it. That scope also confers project deletion, which a licence-cleanup app has no business holding. If a project-grant revocation returns 403, the action is marked failed with the API’s message and written to the audit log — it fails loudly, not silently.
External access
| Host | Why |
|---|---|
api.atlassian.com | The Atlassian Admin (Organizations) REST API — the only first-party source of org-wide last-active data. Called with an organization API key the administrator supplies in-app, not with Forge’s own credentials. |
Data stored
| Record | Key | Contents | Retention |
|---|---|---|---|
| Organization API key | Forge encrypted secret store | The credential, validated against the API before it is stored | Until replaced. Never returned to the browser; only the last four characters are shown back |
| Scans and plans | Forge KVS | Per-user classification, billable paths, activity dates, service-account signals, and the actions a plan proposes | While the app is installed |
| Git activity | one storage write per event | The date of the most recent push or pull-request event per account | From installation onwards; no history is backfilled |
| Exclusions and settings | Forge KVS | Exclusion rules, thresholds, schedule, notification settings and per-seat cost | While the app is installed |
| Audit log | append-only | Every scan, plan, applied action, restore and failure, with the permission level that was removed | While the app is installed |
Security notes
Credential handling
- The organization API key is validated against the API before it is stored, written with Forge’s encrypted secret store, never returned to the browser, and only ever sent to
api.atlassian.com. - Only the last four characters are shown back, so an administrator can confirm which key is installed without being able to read it.
Irreversibility is designed out
- The app never deletes an account and never performs an irreversible operation. The worst case is a revoked permission grant, which is one button away from being restored.
- Every applied action records the permission level it removed, so restore is an exact replay rather than a best guess.
- Safe mode produces the identical plan object Live executes — there is no separate code path that could diverge.
Safety rails
- A recent-activity safety window (default 30 days): any push or pull request inside the window makes a user un-flaggable regardless of login history.
- Service-account confidence threshold (default 50%), calibrated so no single weak signal can quarantine a human, but the strong “writes with no login” signal can on its own.
- Accounts the Admin API cannot measure — anyone outside a verified domain — are classified
unknown, shown as “No data”, and are never auto-actioned. - The plan surfaces a “Skipped by safety rails” list alongside itself, so what the app refused to touch is as visible as what it proposes.
Scope restraint
admin:project:bitbucketis not requested, because it also confers project deletion. A project-grant revocation that needs it fails loudly with the API’s own message rather than the app quietly holding a scope it should not.read:pullrequest:bitbucketis held only to receive events. It backs no REST call in this app.
Testing
- Tests cover the logic that carries the safety guarantees: classification, service-account detection, billable-path derivation, exclusion evaluation, the plan safety rails, schedule due-calculation and CSV escaping.
- They run without a Forge runtime; the platform packages are stubbed.
Known limitations
- Group membership is not readable. Bitbucket exposes user groups only through the deprecated 1.0 API, which Forge apps cannot call. The app therefore cannot remove a user from a group: group-derived paths are labelled “Group grant (inferred)”, marked non-removable, and carry instructions pointing at workspace settings. Membership is inferred from effect. For a hard guarantee, use a user or pattern exclusion.
- Workspace membership cannot be removed via the API. Bitbucket does not support it; membership is managed in Atlassian admin or via SCIM. The app reports these paths and says where to go.
- Project-grant revocation is unverified against a live workspace, because the scope it may additionally require also confers project deletion and is deliberately not requested. A 403 is recorded as a failed action with the API’s message.
- The Admin API only covers managed accounts. External collaborators and contractors on personal accounts return no record, are classified
unknown, and are never auto-actioned. - Last-active data lags up to 24 hours and counts only interactive product use — which is exactly why the git-activity event triggers exist.
- Git activity starts at installation. An identity that pushed last month but not since installation has no write date yet. Do not act on the first scan of a fresh install.
- The repository access index is eventually consistent. It builds in bounded background batches and parks itself when the workspace nears its hourly API limit. Inactivity results are unaffected, and the Overview tab states where the index has got to.
- Notifications are recorded, not emailed. Forge has no first-party outbound email API. Enabling notifications still changes behaviour end to end — the first plan for a flagged user records a notice with a deadline instead of revoking anything, and every later plan refuses to touch that user until the lead time elapses. Only delivery is missing.
- CSV export renders for copy, not download. UI Kit is sandboxed with no filesystem or Blob access. The content is byte-identical to a download.
- Data Center is out of scope. This is a Forge Cloud app.
Release notes
Version history for User License Cleanup & Dashboard for Bitbucket. The most recent release is listed first.
-
v1.0.0 Current
Initial release.
What it does
- Inactive and never-logged-in seat detection for a Bitbucket Cloud workspace, with a per-user billable-path breakdown that says which grant to remove and whether the app can remove it.
- Service-account detection built for Bitbucket specifically: push and pull-request product events distinguish a token-driven CI identity from an abandoned human account.
- Safe/Live cleanup with a shared plan object, typed confirmation, and an exact restore of every applied change.
- Exclusion rules by user, pattern and synthesised group label, with the Excluded tab showing which rule or heuristic spared each user.
- Scheduled scans (default weekly, report only), an append-only audit log, and CSV export from any view.
Defaults, and the reasoning
- Inactivity threshold 90 days — long enough to survive parental leave, sabbaticals and long holidays; short enough that a quarterly audit is meaningful. 30 days produces too many false positives in engineering organisations.
- Never-logged-in grace 14 days — an invited user who has not accepted yet is a pending invite, not waste.
- Recent-activity safety window 30 days — longer than the slowest common CI cadence plus margin.
- Service-account confidence threshold 50%, notification lead time 7 days, scan cadence weekly, report only.
Deliberate omissions
admin:project:bitbucketis not requested, because it also confers project deletion.- No commit or pull-request history is read; git activity comes from product events, which cost nothing and cannot exhaust a rate limit.
- No account is ever deleted, and no irreversible operation exists in the codebase.