Reveal temp passwords (non-prod) and mask OTP/reset destinations - #179
Reveal temp passwords (non-prod) and mask OTP/reset destinations#179goshacodes wants to merge 5 commits into
Conversation
Lets admins reveal a generated temporary password directly in the console instead of only delivering it by email/SMS, gated to non-production environments end-to-end: - auth: DeliveryChannel.show; PasswordService returns the plaintext for 'show' and rejects it in prod with PasswordRevealForbidden. - central: mirrors the prod guard so a tampered client can't reach auth's reveal path even if central's own check were bypassed. - edge: PermissionsResponse now reports isProd, sourced from EnvName. - central-ui: users-list offers a 'Show — display it here' option only when the console reports non-prod, then displays the password masked behind a checkbox with a copy-to-clipboard action. Tests updated in auth/central/edge specs and central-ui Playwright suite (adds show-channel and prod-hides-show-channel cases).
🤖 Augment PR SummarySummary: 🤖 Was this summary useful? React with 👍 or 👎 |
- util: new Masking.email/phone (industry-conventional masking:
first+last char of the local part for email, country code + last
two digits for phone) with a MaskingSpec unit suite.
- auth: StepView.Otp gains a masked destination, computed in
ConversationRenderService from the conversation's resolved
credential (email or phone) before it's serialized into
window.__VERSOLA_FORM__ — the unmasked value never reaches the
client. Covers both a user-entered credential and a login+password
flow's OTP-as-second-factor (userEmail/userPhone already flow into
ConversationRecord.credential upstream).
- central-ui: otp.tsx renders 'We sent a verification code to
{destination}', falling back to the previous generic copy when no
destination is available (i.e. previews, since destination isn't a
backend-configurable property). i18n added for en/ru.
- central/BootstrapService: defaultForms gains an overrideOnBoot flag
so a form already present can still be re-seeded (as a new version)
when its shipped resources changed, without touching every other
form a tenant may have customized. otp opts in to ship this change
to existing environments. Extracted the pure decision as
shouldCheckForSeed with unit tests.
38de2e5 to
f9a9c9a
Compare
Summary
Two related admin-console/UX changes, both gated so nothing extra is exposed in production:
showdelivery channel for password reset, letting admins reveal a generated temporary password directly in the console instead of only delivering it by email/SMS — available only outside production.Maskingutility.Part 1 — "show" delivery channel
auth
DeliveryChannelgainsshow.PasswordRevealForbiddenerror.PasswordService.resetPasswordreturnsOption[Password]: plaintext forshow,Noneforemail/sms. Fails withPasswordRevealForbiddenifshowis requested whileenv.isProd.UserControllerreturns200 {"password": ...}or204.central
DeliveryChannel.show+ response payload threaded throughAuthClient/UserService/UserController.UserControllerre-rejectsshowin prod (404), mirroring the existing guard, so a tampered client can't reach the reveal path even if central's own check were bypassed.edge
PermissionsResponsenow reportsisProd, sourced fromEnvName, so the console can hide non-prod-only affordances.central-ui
admin-appreadsisProdfrom/permissions/me(defaults totrueon load failure) and passescanRevealPassworddown.users-listoffers a "Show — display it here" option only when non-prod, then displays the password masked behind a "Show password" checkbox with a copy-to-clipboard action.Part 2 — masked OTP destination
util
versola.util.Maskingwithemail/phone, following industry conventions: email keeps the first/last char of the local part (j***n@example.com, fully masked if ≤ 2 chars); phone keeps the country calling code and last two digits (+1********34).auth
StepView.Otpgains a maskeddestination: Option[String], computed inConversationRenderServicefrom the conversation's resolved credential (ConversationRecord.credential) before it's serialized intowindow.__VERSOLA_FORM__— the unmasked value never reaches the client. Covers both a user-entered credential and a login+password flow's OTP-as-second-factor.central-ui
otp.tsxrenders "We sent a verification code to {destination}", falling back to the previous generic copy when no destination is available (form previews, sincedestinationisn't a backend-configurable property). i18n added for en/ru.central BootstrapService
defaultFormsgains anoverrideOnBootflag so a form already present can still be re-seeded (as a new version) when its shipped resources changed, without touching every other form a tenant may have customized.otpopts in, so this change reaches existing environments. The decision is a pureshouldCheckForSeedfunction with unit tests, and re-seeding is a no-op if the shipped content is unchanged (so restarts don't churn out empty versions).Testing
PasswordServiceSpec, auth/centralUserControllerSpec,AuthClientSpec,EdgeServiceSpec/EdgeControllerSpec,ConversationRenderServiceSpec(new destination-masking cases),BootstrapServiceSpec(newshouldCheckForSeedcases); newMaskingSpec.Reset Password offers the show channel outside production/hides the show channel in production. Full Playwright suite passes except twoapp-shell.spec.tsfailures that pre-exist onmain(unrelated).sbt testbefore merging.