You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bug: member password flows validate against the default policy — password_policy reads are admin-gated (+ FORBIDDEN preload noise on policies-limits) #3098
Non-admin members cannot read password_policy, but member-facing password flows read it to validate the member's OWN new password. The hook silently falls back to the default policy while the server enforces the org's real one: the form validates against rules the server won't enforce, accepts passwords the server then rejects, and the member is never shown the actual requirements. Separately, the Policies & Limits route loader preloads five admin-gated policy types for everyone, minting FORBIDDEN error events on hover/visit.
Not covered by the Unauthenticated-flood work (UNAUTHENTICATED class) or the empty-org-id sweep (#3086): this is the FORBIDDEN class, hit by logged-in non-admins.
Quiet since 08-12, but the trigger is any non-admin member using a password flow or visiting Policies & Limits — latent, and the password half is a correctness bug, not just noise.
Lane 1 — functional bug (password UX). Member-facing password flows read password_policy for the CURRENT user's own password:
app/routes/forced-change-password.$id.tsx:51 — usePasswordPolicy(organizationId); every non-admin forced through this flow triggers FORBIDDEN.
app/features/settings/account/components/account-form.tsx:271 and :436 — self-service password change in account settings.
(member-add-dialog.tsx:61 is admin-context — fine.)
usePasswordPolicy (app/features/settings/governance/hooks/queries.ts:100-118) silently falls back to DEFAULT_PASSWORD_POLICY when the read errors. Meanwhile the server enforces the org's REAL policy on submit (convex/users/update_user_password.ts:63 → passwordPolicyViolations(args.newPassword, policy) → password_policy_violation).
Lane 2 — FORBIDDEN noise.app/routes/dashboard/$id/settings/governance/policies-limits.tsx:20-30 loader preloads nine policy types, five of them admin-gated (budgets, retention_policy, voice_output, sandbox_quota, conversation_routing). ensureGovernancePolicies (app/lib/loader-preload.ts:61-80) swallows only UNAUTHENTICATED; FORBIDDEN escapes, and each preload lands server-side as an uncaught error event. With defaultPreload: 'intent' (router.tsx) hovering the nav link is enough. The useAbility mask in governance/route.tsx runs at render time — after the loader — so it gates nothing here.
Fix direction
Give password flows a member-readable projection of the org password policy: a dedicated self-serve query (e.g. getPasswordPolicyForSelf returning only complexity rules), or add password_policy to POLICY_TYPES_READABLE_BY_MEMBER if nothing beyond the rules lives in it (precedent: session_idle_timeout, added for the idle watchdog, Feature: Session idle timeout (server-side enforcement + client watchdog) #1502).
Make usePasswordPolicy's fallback honest — while the effective policy is unreadable, don't validate against defaults the server won't enforce (surface the rules from the server error, or block on the real policy).
Stop preloading admin-gated policy types for non-admins on policies-limits (preload only the member-readable subset, or gate the loader on ability), and/or teach ensureGovernancePolicies to treat FORBIDDEN as an expected rejection like it already does UNAUTHENTICATED.
Acceptance
A non-admin member changing their password (forced-change and account settings) validates against the org's actual complexity rules and sees them in the UI; no new FORBIDDEN events from these flows.
A member visiting or hovering Governance → Policies & Limits produces no new FORBIDDEN events; issues 7252143 / 7266821 stay flat after deploy.
Admin editors behave unchanged.
Filed by the GlitchTip triage sweep of demo.tale.dev (2026-08-28). Tracked on the tale board as TAL-21 — "Member password-change flows validate against default policy because password_policy reads are admin-gated (+ FORBIDDEN preload noise on policies-limits)" (status: todo).
Problem
Non-admin members cannot read
password_policy, but member-facing password flows read it to validate the member's OWN new password. The hook silently falls back to the default policy while the server enforces the org's real one: the form validates against rules the server won't enforce, accepts passwords the server then rejects, and the member is never shown the actual requirements. Separately, the Policies & Limits route loader preloads five admin-gated policy types for everyone, minting FORBIDDEN error events on hover/visit.Not covered by the Unauthenticated-flood work (UNAUTHENTICATED class) or the empty-org-id sweep (#3086): this is the FORBIDDEN class, hit by logged-in non-admins.
Evidence (GlitchTip, demo.tale.dev)
ConvexError FORBIDDEN: "Reading password_policy requires admin role."— 20 events, 2026-07-06 → 2026-08-12,governance/queries.ts."Reading voice_output requires admin role."— 5 events, 2026-07-07 → 2026-08-12.Quiet since 08-12, but the trigger is any non-admin member using a password flow or visiting Policies & Limits — latent, and the password half is a correctness bug, not just noise.
Root cause (verified at HEAD 2026-08-27)
services/platform/convex/governance/queries.ts—POLICY_TYPES_READABLE_BY_MEMBERdeliberately excludespassword_policy,voice_output,budgets,retention_policy,sandbox_quota,conversation_routing;getPolicythrows FORBIDDEN for non-admin members.Lane 1 — functional bug (password UX). Member-facing password flows read
password_policyfor the CURRENT user's own password:app/routes/forced-change-password.$id.tsx:51—usePasswordPolicy(organizationId); every non-admin forced through this flow triggers FORBIDDEN.app/features/settings/account/components/account-form.tsx:271and:436— self-service password change in account settings.member-add-dialog.tsx:61is admin-context — fine.)usePasswordPolicy(app/features/settings/governance/hooks/queries.ts:100-118) silently falls back toDEFAULT_PASSWORD_POLICYwhen the read errors. Meanwhile the server enforces the org's REAL policy on submit (convex/users/update_user_password.ts:63→passwordPolicyViolations(args.newPassword, policy)→password_policy_violation).Lane 2 — FORBIDDEN noise.
app/routes/dashboard/$id/settings/governance/policies-limits.tsx:20-30loader preloads nine policy types, five of them admin-gated (budgets,retention_policy,voice_output,sandbox_quota,conversation_routing).ensureGovernancePolicies(app/lib/loader-preload.ts:61-80) swallows only UNAUTHENTICATED; FORBIDDEN escapes, and each preload lands server-side as an uncaught error event. WithdefaultPreload: 'intent'(router.tsx) hovering the nav link is enough. TheuseAbilitymask ingovernance/route.tsxruns at render time — after the loader — so it gates nothing here.Fix direction
getPasswordPolicyForSelfreturning only complexity rules), or addpassword_policytoPOLICY_TYPES_READABLE_BY_MEMBERif nothing beyond the rules lives in it (precedent:session_idle_timeout, added for the idle watchdog, Feature: Session idle timeout (server-side enforcement + client watchdog) #1502).usePasswordPolicy's fallback honest — while the effective policy is unreadable, don't validate against defaults the server won't enforce (surface the rules from the server error, or block on the real policy).ensureGovernancePoliciesto treat FORBIDDEN as an expected rejection like it already does UNAUTHENTICATED.Acceptance
Filed by the GlitchTip triage sweep of demo.tale.dev (2026-08-28). Tracked on the tale board as TAL-21 — "Member password-change flows validate against default policy because password_policy reads are admin-gated (+ FORBIDDEN preload noise on policies-limits)" (status: todo).