feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
438
server/supabase/tests/team-push-security.integration.sql
Normal file
438
server/supabase/tests/team-push-security.integration.sql
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
\set ON_ERROR_STOP on
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF condition IS NOT TRUE THEN
|
||||
RAISE EXCEPTION 'assertion_failed: %', message;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO auth.users (
|
||||
id, aud, role, email, encrypted_password, email_confirmed_at,
|
||||
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
|
||||
) VALUES
|
||||
(
|
||||
'30000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
|
||||
'team-owner@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
|
||||
'{"provider":"email","providers":["email"]}'::jsonb,
|
||||
'{"name":"Team Owner"}'::jsonb, now(), now()
|
||||
),
|
||||
(
|
||||
'30000000-0000-4000-8000-000000000002', 'authenticated', 'authenticated',
|
||||
'team-member@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
|
||||
'{"provider":"email","providers":["email"]}'::jsonb,
|
||||
'{"name":"Team Member"}'::jsonb, now(), now()
|
||||
),
|
||||
(
|
||||
'30000000-0000-4000-8000-000000000003', 'authenticated', 'authenticated',
|
||||
'team-invitee@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
|
||||
'{"provider":"email","providers":["email"]}'::jsonb,
|
||||
'{"name":"Team Invitee"}'::jsonb, now(), now()
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
has_function_privilege('authenticated', 'public.create_team(text)', 'EXECUTE'),
|
||||
'authenticated may call atomic team creation'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_function_privilege('anon', 'public.create_team(text)', 'EXECUTE'),
|
||||
'anonymous may not create teams'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_table_privilege('authenticated', 'public.push_dispatch_attempts', 'SELECT'),
|
||||
'dispatch audit ledger is service-only'
|
||||
);
|
||||
|
||||
SET LOCAL ROLE authenticated;
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000001","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
INSERT INTO public.teams (name, owner_id)
|
||||
VALUES ('Direct write must fail', auth.uid());
|
||||
RAISE EXCEPTION 'direct_team_insert_was_allowed';
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT (public.create_team(' Security Team ')->>'id')::uuid AS team_id \gset
|
||||
SELECT set_config('test.team_id', :'team_id', true);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.teams WHERE id = :'team_id') = 1,
|
||||
'atomic create returns a readable team'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.team_members
|
||||
WHERE team_id = :'team_id'
|
||||
AND user_id = auth.uid()
|
||||
AND role = 'owner') = 1,
|
||||
'atomic create installs exactly one owner membership'
|
||||
);
|
||||
|
||||
SELECT public.create_team_invite(
|
||||
:'team_id',
|
||||
'team-member@example.invalid',
|
||||
'member'
|
||||
) AS first_invite \gset
|
||||
SELECT (:'first_invite'::jsonb->>'token') AS member_invite_token \gset
|
||||
SELECT (:'first_invite'::jsonb->>'id')::uuid AS member_invite_id \gset
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.team_invites WHERE team_id = :'team_id') = 0,
|
||||
'team members cannot read bearer invite tokens directly'
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
public.resolve_team_invite_recipient(:'member_invite_id') =
|
||||
'30000000-0000-4000-8000-000000000002'::uuid,
|
||||
'inviter can resolve an active invite recipient without exposing auth.users'
|
||||
);
|
||||
|
||||
-- A repeated active invite is idempotent and returns the same token.
|
||||
SELECT public.create_team_invite(
|
||||
:'team_id',
|
||||
'TEAM-MEMBER@example.invalid',
|
||||
'member'
|
||||
) AS duplicate_invite \gset
|
||||
SELECT pg_temp.assert_true(
|
||||
:'duplicate_invite'::jsonb->>'token' = :'member_invite_token'
|
||||
AND (:'duplicate_invite'::jsonb->>'duplicate')::boolean,
|
||||
'active invite replay is idempotent and case-normalized'
|
||||
);
|
||||
|
||||
-- The caller-bound RLS helpers must not disclose another account's teams.
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.user_team_ids(
|
||||
'30000000-0000-4000-8000-000000000002'::uuid
|
||||
)) = 0,
|
||||
'team helper rejects an arbitrary requested user id'
|
||||
);
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000002","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
public.resolve_team_invite_recipient(:'member_invite_id') IS NULL,
|
||||
'invite recipient cannot use inviter-only recipient resolution'
|
||||
);
|
||||
|
||||
SELECT public.accept_team_invite(:'member_invite_token') AS accepted_invite \gset
|
||||
SELECT pg_temp.assert_true(
|
||||
(:'accepted_invite'::jsonb->>'role') = 'member'
|
||||
AND NOT (:'accepted_invite'::jsonb->>'duplicate')::boolean,
|
||||
'matching recipient accepts the invite atomically'
|
||||
);
|
||||
|
||||
SELECT public.accept_team_invite(:'member_invite_token') AS accepted_replay \gset
|
||||
SELECT pg_temp.assert_true(
|
||||
(:'accepted_replay'::jsonb->>'duplicate')::boolean
|
||||
AND (:'accepted_replay'::jsonb->>'already_member')::boolean,
|
||||
'same recipient acceptance replay is idempotent'
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.list_team_members(:'team_id')) = 2,
|
||||
'members can list the limited team profile projection'
|
||||
);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
affected_rows integer;
|
||||
BEGIN
|
||||
UPDATE public.team_members
|
||||
SET role = 'owner'
|
||||
WHERE team_id = current_setting('test.team_id')::uuid AND user_id = auth.uid();
|
||||
GET DIAGNOSTICS affected_rows = ROW_COUNT;
|
||||
IF affected_rows <> 0 THEN
|
||||
RAISE EXCEPTION 'direct_role_update_was_allowed';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM public.update_team_member_role(
|
||||
current_setting('test.team_id')::uuid,
|
||||
'30000000-0000-4000-8000-000000000001',
|
||||
'member'
|
||||
);
|
||||
RAISE EXCEPTION 'member_changed_owner_role';
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000001","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
public.update_team_member_role(
|
||||
:'team_id',
|
||||
'30000000-0000-4000-8000-000000000002',
|
||||
'admin'
|
||||
)->>'role' = 'admin',
|
||||
'owner can promote a non-owner to admin'
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM public.remove_team_member(
|
||||
current_setting('test.team_id')::uuid,
|
||||
'30000000-0000-4000-8000-000000000001'
|
||||
);
|
||||
RAISE EXCEPTION 'owner_was_removed';
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT public.create_team_invite(
|
||||
:'team_id',
|
||||
'team-invitee@example.invalid',
|
||||
'member'
|
||||
) AS second_invite \gset
|
||||
SELECT (:'second_invite'::jsonb->>'token') AS invitee_token \gset
|
||||
SELECT (:'second_invite'::jsonb->>'id')::uuid AS invitee_id \gset
|
||||
SELECT set_config('test.invitee_token', :'invitee_token', true);
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000002","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM public.create_team_invite(
|
||||
current_setting('test.team_id')::uuid,
|
||||
'another-admin@example.invalid',
|
||||
'admin'
|
||||
);
|
||||
RAISE EXCEPTION 'admin_invited_another_admin';
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000003","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
public.accept_team_invite(:'invitee_token')->>'role' = 'member',
|
||||
'second matching recipient can join'
|
||||
);
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000002","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
(public.remove_team_member(
|
||||
:'team_id',
|
||||
'30000000-0000-4000-8000-000000000003'
|
||||
)->>'removed')::boolean,
|
||||
'admin can remove a regular member'
|
||||
);
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000003","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM public.accept_team_invite(current_setting('test.invitee_token'));
|
||||
RAISE EXCEPTION 'removed_member_reused_an_accepted_invite';
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000002","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
-- Push registration is bound to an active device and hidden by RLS.
|
||||
INSERT INTO public.devices (
|
||||
id, user_id, installation_id, platform, device_name, app_version, os_version
|
||||
) VALUES (
|
||||
'31000000-0000-4000-8000-000000000002',
|
||||
auth.uid(),
|
||||
'32000000-0000-4000-8000-000000000002',
|
||||
'android', 'Member Phone', '1.0', 'Android test'
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
public.register_push_registration(
|
||||
'31000000-0000-4000-8000-000000000002',
|
||||
'fcm',
|
||||
'fixture-fcm-registration-token-member-0002'
|
||||
)->>'provider' = 'fcm',
|
||||
'active Android device registers an FCM token atomically'
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.push_tokens) = 0,
|
||||
'authenticated users cannot read raw push registrations'
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
UPDATE public.devices
|
||||
SET push_token = 'direct-token-must-fail-000000000000'
|
||||
WHERE id = '31000000-0000-4000-8000-000000000002';
|
||||
RAISE EXCEPTION 'direct_device_push_token_update_was_allowed';
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT set_config(
|
||||
'request.jwt.claims',
|
||||
'{"sub":"30000000-0000-4000-8000-000000000001","role":"authenticated"}',
|
||||
true
|
||||
);
|
||||
|
||||
INSERT INTO public.devices (
|
||||
id, user_id, installation_id, platform, device_name, app_version, os_version
|
||||
) VALUES (
|
||||
'31000000-0000-4000-8000-000000000001',
|
||||
auth.uid(),
|
||||
'32000000-0000-4000-8000-000000000001',
|
||||
'android', 'Owner Phone', '1.0', 'Android test'
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM public.register_push_registration(
|
||||
'31000000-0000-4000-8000-000000000001',
|
||||
'fcm',
|
||||
'fixture-fcm-registration-token-member-0002'
|
||||
);
|
||||
RAISE EXCEPTION 'other_user_push_token_was_stolen';
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT public.reserve_push_dispatch(
|
||||
'transcription.completed',
|
||||
'33000000-0000-4000-8000-000000000001'
|
||||
) AS first_reservation \gset
|
||||
SELECT pg_temp.assert_true(
|
||||
(:'first_reservation'::jsonb->>'reserved')::boolean
|
||||
AND NOT (:'first_reservation'::jsonb->>'duplicate')::boolean,
|
||||
'first push dispatch is reserved'
|
||||
);
|
||||
|
||||
SELECT public.reserve_push_dispatch(
|
||||
'transcription.completed',
|
||||
'33000000-0000-4000-8000-000000000001'
|
||||
) AS duplicate_reservation \gset
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT (:'duplicate_reservation'::jsonb->>'reserved')::boolean
|
||||
AND (:'duplicate_reservation'::jsonb->>'duplicate')::boolean,
|
||||
'same push event is deduplicated for thirty seconds'
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM public.reserve_push_dispatch(
|
||||
'arbitrary.message',
|
||||
'33000000-0000-4000-8000-000000000099'
|
||||
);
|
||||
RAISE EXCEPTION 'unsupported_push_event_was_reserved';
|
||||
EXCEPTION
|
||||
WHEN invalid_parameter_value THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT public.reserve_push_dispatch(
|
||||
'billing.status.changed',
|
||||
('33000000-0000-4000-8000-' || lpad(number::text, 12, '0'))::uuid
|
||||
)
|
||||
FROM generate_series(2, 10) AS number;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
PERFORM public.reserve_push_dispatch(
|
||||
'billing.status.changed',
|
||||
'33000000-0000-4000-8000-000000000011'
|
||||
);
|
||||
RAISE EXCEPTION 'push_rate_limit_was_not_enforced';
|
||||
EXCEPTION
|
||||
WHEN program_limit_exceeded THEN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
RESET ROLE;
|
||||
|
||||
SET LOCAL ROLE service_role;
|
||||
SELECT set_config('request.jwt.claims', '{"role":"service_role"}', true);
|
||||
SELECT public.reserve_system_push_dispatch(
|
||||
'transcription.completed',
|
||||
'34000000-0000-4000-8000-000000000001'
|
||||
) AS system_reservation \gset
|
||||
SELECT pg_temp.assert_true(
|
||||
(:'system_reservation'::jsonb->>'reserved')::boolean,
|
||||
'service role reserves an internal system notification'
|
||||
);
|
||||
SELECT public.reserve_system_push_dispatch(
|
||||
'transcription.completed',
|
||||
'34000000-0000-4000-8000-000000000001'
|
||||
) AS system_replay \gset
|
||||
SELECT pg_temp.assert_true(
|
||||
(:'system_replay'::jsonb->>'duplicate')::boolean
|
||||
AND NOT (:'system_replay'::jsonb->>'reserved')::boolean,
|
||||
'system notification replay is deduplicated'
|
||||
);
|
||||
RESET ROLE;
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.team_members
|
||||
WHERE team_id = :'team_id' AND role = 'owner') = 1,
|
||||
'team retains exactly one immutable owner'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.push_tokens) = 1,
|
||||
'token theft attempt leaves the original registration intact'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.push_dispatch_attempts
|
||||
WHERE caller_id = '30000000-0000-4000-8000-000000000001') = 10,
|
||||
'dedupe does not consume an additional rate-limit slot'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
(SELECT count(*) FROM public.push_dispatch_attempts
|
||||
WHERE actor_kind = 'system'
|
||||
AND event_type = 'transcription.completed'
|
||||
AND resource_id = '34000000-0000-4000-8000-000000000001') = 1,
|
||||
'system dispatch ledger contains exactly one reservation'
|
||||
);
|
||||
|
||||
ROLLBACK;
|
||||
Loading…
Add table
Add a link
Reference in a new issue