Skip to content

fix: self-heal Flash backend service-account role grants on migrate#54

Merged
islandbitcoin merged 1 commit into
mainfrom
fix/service-account-role-selfheal
Jul 15, 2026
Merged

fix: self-heal Flash backend service-account role grants on migrate#54
islandbitcoin merged 1 commit into
mainfrom
fix/service-account-role-selfheal

Conversation

@islandbitcoin

Copy link
Copy Markdown
Contributor

Problem

The Flash backend service account needs two ERPNext roles to serve cashout and the account-upgrade flow:

  • Flash Admin — gates the admin_panel custom doctypes (Account Upgrade Request, Bank Account Update Request)
  • Accounts Manager — gates the standard ERPNext doctypes the backend reads (Bank Account, Currency Exchange, Customer, Journal Entry, Bank)

Role definitions ship in doctype JSON and are versioned/safe. Role assignments on the User record are not versioned — and this week the Flash Admin assignment silently dropped, 403'ing cashout and the upgrade flow until an operator re-granted it by hand. Nothing in the app re-asserted it.

after_migrate() already called ensure_roles(), but that only creates the Flash Admin Role record — it never assigns it to the service account. That was the gap.

Fix

Add ensure_service_account_roles() to after_migrate, so the assignment re-asserts on every bench migrate (i.e. every deploy):

  • Per-env candidate listflash_sa@getflash.io (prod) and flash-service-account@getflash.io (test). Whichever User exists on the site gets the roles; the other is simply absent. A flash_service_account key in site_config overrides the list without a code change.
  • Idempotent — only calls add_roles() for a role that's actually missing, so a converged site does no needless User.save() per migrate.
  • No privilege-escalation heuristic — deliberately does not grant based on "any user with an api_key." Live data shows non-service users (e.g. a human admin) also hold api_keys, so that heuristic would hand them the service roles. The candidate list is explicit.

Placed in after_migrate (runs every migrate, correct for a self-healing invariant) rather than a Frappe patch (runs once via patch_log — wrong for self-heal).

Testing

Acceptance-tested on TEST ERPNext:

Step Service-account roles
Before Accounts Manager, Flash Admin
After dropping Flash Admin (simulated regression) Accounts Manager
After ensure_service_account_roles() Accounts Manager, Flash Admin
After re-run (idempotency) Accounts Manager, Flash Admin ✓ (no write)

python3 -m py_compile clean; no mixed tab/space indentation.

Deploy

Ships on the next ERP image tag → values.base.yaml bump → make erp. The self-heal takes effect during that deploy's bench migrate.

🤖 Generated with Claude Code

The Flash Admin + Accounts Manager role *assignments* on the backend
service-account User are not versioned (unlike the DocPerm role
*definitions*, which ship in doctype JSON). They silently dropped once
this week, which 403'd cashout and the account-upgrade flow until
re-granted by hand.

Add ensure_service_account_roles() to after_migrate so the assignment
re-asserts on every `bench migrate`:

- Hardcoded 2-email candidate list (flash_sa@ prod / flash-service-account@
  test); whichever exists on the site gets the roles, the other is absent.
  A `flash_service_account` site_config key overrides without a code change.
- Idempotent: only calls add_roles() when a role is actually missing, so a
  converged site does no needless User.save() per migrate.
- Deliberately does NOT auto-grant based on "any user with an api_key" —
  live data shows non-service users hold api_keys too, so that heuristic
  would be a privilege-escalation vector.

Acceptance-tested on TEST: dropped Flash Admin from the service account,
ran the function, confirmed it restored the role and was a no-op on re-run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@islandbitcoin
islandbitcoin merged commit 518b9da into main Jul 15, 2026
1 check passed
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.

1 participant