Skip to content

Bug: member password flows validate against the default policy — password_policy reads are admin-gated (+ FORBIDDEN preload noise on policies-limits) #3098

Description

@larryro

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)

  • Issue 7252143ConvexError FORBIDDEN: "Reading password_policy requires admin role."20 events, 2026-07-06 → 2026-08-12, governance/queries.ts.
  • Issue 7266821"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.tsPOLICY_TYPES_READABLE_BY_MEMBER deliberately excludes password_policy, voice_output, budgets, retention_policy, sandbox_quota, conversation_routing; getPolicy throws FORBIDDEN for non-admin members.

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:51usePasswordPolicy(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:63passwordPolicyViolations(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

  1. 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).
  2. 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).
  3. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions