fix(secrets): honor ops.secrets.read permission for reveal/list
A regular member granted reveal access either via the ops.secrets.read role permission or a per-secret ops_acl entry could not actually see or reveal secrets: revealSecret hard-gated on built-in role admin/board (ignoring both the permission and the ACL), and secrets_select RLS never consulted the permission, and the Ops page short-circuited the secrets fetch by built-in role. Migration 031 additively adds an OR user_has_permission(..., 'ops.secrets.read') branch to secrets_select; the admin/board and per-secret ops_acl branches are unchanged so deployment is access-neutral unless an admin has granted the permission. revealSecret now resolves the member and lets RLS be the boundary (a denied row simply does not return). The Ops page always queries secrets (RLS filters the set) and shows the section to admin/board, a permission holder, or anyone with an ACL-granted secret. Write paths (create/update/delete) are unchanged. Mirrored in schema.sql; migration docs and HANDOFF updated.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
-- =============================================================================
|
||||
-- 031: secrets SELECT also honors the ops.secrets.read role permission
|
||||
-- =============================================================================
|
||||
-- Before this migration, secrets_select (from 023) allowed a row only to
|
||||
-- admin/board OR a subject named in a per-secret ops_acl entry. The role
|
||||
-- permission ops.secrets.read ("Reveal secrets" in the Customize matrix) was
|
||||
-- NEVER consulted for secrets, so granting it did nothing and the matrix was
|
||||
-- misleading. This adds the missing additive branch.
|
||||
--
|
||||
-- This is purely ADDITIVE and access-neutral for existing deployments:
|
||||
-- * The admin/board branch is unchanged.
|
||||
-- * The per-secret ops_acl branch is unchanged.
|
||||
-- * A new OR branch grants SELECT to a member whose effective role holds
|
||||
-- ops.secrets.read in space_role_permissions (admin implicitly holds all,
|
||||
-- already covered by user_has_permission). With no such grant present the
|
||||
-- behavior is exactly as before, so no space loses or gains access by
|
||||
-- deploying this unless an admin has explicitly granted the permission.
|
||||
-- * Still space-scoped: user_has_permission is per (uid, space_id), so this
|
||||
-- cannot widen access across tenants.
|
||||
--
|
||||
-- The reveal/list server actions are updated in the same change to let RLS be
|
||||
-- the boundary instead of a hard built-in-role pre-check (lib/actions/
|
||||
-- secrets.ts). Write paths (create/update/delete) are intentionally NOT
|
||||
-- widened.
|
||||
--
|
||||
-- Idempotent: DROP POLICY IF EXISTS then CREATE. The policy body is the
|
||||
-- 023/030 text verbatim plus one OR branch.
|
||||
-- =============================================================================
|
||||
|
||||
DROP POLICY IF EXISTS secrets_select ON public.secrets;
|
||||
CREATE POLICY secrets_select ON public.secrets FOR SELECT
|
||||
USING (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
||||
OR public.user_has_permission(auth.uid(), space_id, 'ops.secrets.read')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.ops_acl a
|
||||
WHERE a.space_id = secrets.space_id AND a.entity_type = 'secret'
|
||||
AND a.entity_id = secrets.id
|
||||
AND a.role IN (SELECT public.user_effective_roles(auth.uid(), secrets.space_id))
|
||||
)
|
||||
);
|
||||
@@ -1782,9 +1782,13 @@ CREATE POLICY ops_acl_delete ON public.ops_acl FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
DROP POLICY IF EXISTS secrets_select ON public.secrets;
|
||||
-- secrets_select also honors the ops.secrets.read role permission
|
||||
-- (equivalent to scripts/031_secrets_permission_select.sql). Additive: the
|
||||
-- admin/board and per-secret ops_acl branches are unchanged.
|
||||
CREATE POLICY secrets_select ON public.secrets FOR SELECT
|
||||
USING (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
||||
OR public.user_has_permission(auth.uid(), space_id, 'ops.secrets.read')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.ops_acl a
|
||||
WHERE a.space_id = secrets.space_id AND a.entity_type = 'secret'
|
||||
|
||||
Reference in New Issue
Block a user