fix(ansible): render STATE_LEVEL_TENANT_ID from inventory (production login regression) - #40
Conversation
…upstream pg/mz defaults
The compose file ships with `STATE_LEVEL_TENANT_ID: pg` (+ `pg.citya`
on the two city-tier services + a stray `mz` on inbox) from upstream
egovernments/CCRS sample data. The playbook copies the compose verbatim
and only rewrites the Grafana domain. Net result: every DIGIT service
comes up using `pg` for state-tier env vars on any tenant deployed
through this playbook, including naipepea and (when redeployed) bomet.
Adds an inventory field `state_root` (the actual country/state tenant
— `ke` for Nairobi/Bomet, distinct from the misleadingly-named
`state_tenant_id` which is the city tenant the SPA pins to) and four
`replace:` tasks that rewrite the upstream defaults:
STATE_LEVEL_TENANT_ID: pg -> {{ state_root }}
EGOV_STATE_LEVEL_TENANT_ID: pg -> {{ state_root }}
STATE_LEVEL_TENANT_ID: pg.citya -> {{ tenant_id }} (city tier)
STATE_LEVEL_TENANT_ID: mz -> {{ state_root }}
Three `pg` defaults stay unrewritten because they match what the working
naipepea backup had pre-regression too — `EGOV_STATELEVEL_TENANTID` on
egov-indexer, `EGOV_STATELEVEL_TENANT` on egov-hrms, and
`PGR_STATELEVEL_TENANTID` on pgr-services. These appear to be unread
or differently-named env vars and were `pg` even on a working stack;
leaving them alone keeps the diff scoped to what actually breaks auth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Superseded by #52 (consolidated Mac bring-up — 16 commits absorbing this PR + 5 others into one canonical path on |
What happened
On 2026-05-14 ~06:23 UTC a deploy of this branch onto naipepea rewrote
/opt/digit/docker-compose.egov-digit.yamlwith the upstream defaults (pg/pg.citya/mzforSTATE_LEVEL_TENANT_IDandEGOV_STATE_LEVEL_TENANT_ID). Login was broken for allke.nairobiusers — and mostkeusers — until manually patched ~25 hours later.The trigger was the playbook's
Copy registry-prefixed Docker Compose filestep doing a verbatimcopy:of the source compose, with no follow-upreplace:task for these env vars. Only Grafana's domain gets rewritten today (api.egov.theflywheel.in→{{ domain }}).How it breaks login
DIGIT services that need a "state-level tenant" read it from env. Most critical:
egov-user. At login, the user-service does this dance:username=ADMIN,tenantId=ke(orke.nairobi).eg_user.usernameis stored encrypted (Kenya DPA). The encrypt is done with the tenant fromSTATE_LEVEL_TENANT_ID, not the request's tenantId.STATE_LEVEL_TENANT_ID=pg, egov-enc-service usespg's symmetric key (id177813) → resulting ciphertext is177813|<base64>.eg_user WHERE username='177813|<base64>'→ no match (the row is stored as74493|<base64>because it was created whenSTATE_LEVEL=ke).UserNotFoundException→OAuth2Exception: Invalid login credentials→ HTTP 400.Real trace pulled from
digit-egov-user-1on naipepea before the fix:Key-id
177813ispg's key, notke.nairobi's.Direct evidence the encrypt service itself was healthy — same machine, two different
tenantIds:So the per-tenant key dispatch on
egov-enc-serviceworks correctly; the bug is upstream:egov-useris asking the wrong tenant.Why only some accounts kept working
Pre-fix
eg_user.usernamekey distribution on naipepea:ADMINwas already stored under177813|(probably from an earlier deploy whenSTATE_LEVEL=pgwas also active — this is a recurring regression). So withSTATE_LEVEL=pgon 2026-05-14, ADMIN happened to work by accidental matching, masking the breakage long enough that the user-facing 400s only started getting reported when a non-ADMIN tried.Manual fix applied on naipepea (pre-this-PR)
Two phases:
sedthe 13STATE_LEVEL_TENANT_ID/EGOV_STATE_LEVEL_TENANT_IDlines in/opt/digit/docker-compose.egov-digit.yamlfrompg/pg.citya/mzback toke/ke.nairobi, thendocker compose up -dto recreate egov-user, egov-enc-service, egov-workflow-v2, boundary-service, default-data-handler, egov-hrms, egov-bndry-mgmnt, pgr-services.177813|(ADMIN + 14 ke.nairobi + 3 ke.care) to74493|so they survive the new lookup tenant:_decrypt→_encryptwithtenantId=ke→UPDATE eg_user. TwokeCITIZEN duplicates of registered mobile numbers couldn't be migrated (unique key collision); harmless — the live mobile-number row already exists under74493|.This PR is the fix-at-source so the next
./deploy.sh nairobi(or any tenant) doesn't reintroduce the regression.What's in this PR
state_root(state-tier tenant —ke,pb,mz) to the host_vars contract, distinguished fromstate_tenant_id(the city-tier tenant the SPA binds to)host_vars/_example.ymlstate_rootdefault + the failure modegroup_vars/digit.ymlreplace:tasks rewriting the compose env values after thecopy:stepplaybook-deploy.ymlThe four substitutions:
Three other
pgenv occurrences in the compose are intentionally left alone (EGOV_STATELEVEL_TENANTIDon egov-indexer,EGOV_STATELEVEL_TENANTon egov-hrms,PGR_STATELEVEL_TENANTIDon pgr-services). Each is either typo'd or differently-named relative to what the services actually consume, and each waspgon the working pre-regression compose too. Keeps this PR scoped to the variables that demonstrably broke auth.Verification
Locally rendered the playbook against a synthetic
state_root: ke/tenant_id: ke.nairobi.grepconfirms all 13 affected lines flip to the inventory values. The 3 leave-alonepglines staypg. No other compose lines change.Test plan for whoever runs the next deploy
./deploy.sh nairobi→docker exec digit-egov-user-1 env | grep STATE_LEVELshowske(notpg).curl POST /user/oauth/tokenwith ake.nairobi-tenant employee returns a 200 + access_token (not 400 invalid_credentials)../deploy.sh bomet→ same, butke.bometuser works (assumesstate_root: keandtenant_id: ke.bometin bomet host_vars).tenantId=kestill works.🤖 Generated with Claude Code