@fayz-ai/db 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canonical-schema.json +106 -23
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +42 -37
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/000_baseline.sql +29667 -0
- package/migrations/001_the_tenant_has_apps.sql +314 -0
- package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
- package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
- package/migrations/004_the_projections_get_a_successor.sql +312 -0
- package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
- package/migrations/006_the_helpers_read_the_source.sql +153 -0
- package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
- package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
- package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
- package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
- package/migrations/011_leadcontrol_joins_the_family.sql +116 -0
- package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
- package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
- package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
- package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
- package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
- package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
- package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
- package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
- package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
- package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
- package/package.json +1 -1
- package/migrations/0000_legacy_quarantine.sql +0 -86
- package/migrations/0000b_straggler_sweep.sql +0 -37
- package/migrations/000_core_v1_convert.sql +0 -343
- package/migrations/001_core.sql +0 -372
- package/migrations/002_rls_user_tenant_ids.sql +0 -64
- package/migrations/003_invitations.sql +0 -17
- package/migrations/004_archetypes.sql +0 -343
- package/migrations/005_locations_archetype.sql +0 -18
- package/migrations/006_archetype_rls.sql +0 -74
- package/migrations/007_archetype_grants.sql +0 -4
- package/migrations/008_grants.sql +0 -5
- package/migrations/009_anon_hardening.sql +0 -19
- package/migrations/010_migration_ledger.sql +0 -46
- package/migrations/011_anon_write_revoke.sql +0 -12
- package/migrations/012_rename_migration_ledger.sql +0 -61
- package/migrations/013_sequences_rls.sql +0 -22
- package/migrations/014_plan_entitlements.sql +0 -28
- package/migrations/015_agent_guard.sql +0 -108
- package/migrations/016_agent_guard_actor.sql +0 -102
- package/migrations/017_core_addresses.sql +0 -115
- package/migrations/018_global_search.sql +0 -482
- package/migrations/019_tenant_members_person_id.sql +0 -20
- package/migrations/020_fayz_projects.sql +0 -63
- package/migrations/021_tenant_roles.sql +0 -74
- package/migrations/022_invite_acceptance.sql +0 -91
- package/migrations/023_team_visibility.sql +0 -75
- package/migrations/024_analytics_engine.sql +0 -317
- package/migrations/025_created_by.sql +0 -75
- package/migrations/026_audit_trail.sql +0 -73
- package/migrations/027_domain_events.sql +0 -266
- package/migrations/028_tenant_scoped_token.sql +0 -127
- package/migrations/029_connections.sql +0 -186
- package/migrations/030_effect_idempotency.sql +0 -159
- package/migrations/031_sync_run_message.sql +0 -39
- package/migrations/032_connection_secrets.sql +0 -227
- package/migrations/033_sync_schedule.sql +0 -651
- package/migrations/034_custom_fields.sql +0 -55
- package/migrations/035_field_registry.sql +0 -148
- package/migrations/036_analytics_run_batch.sql +0 -84
- package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
- package/migrations/038_onboarding_responses.sql +0 -103
- package/migrations/039_unit_tree.sql +0 -270
- package/migrations/040_resource_grants.sql +0 -474
- package/migrations/041_scoped_columns.sql +0 -192
- package/migrations/042_unit_scope_policies.sql +0 -145
- package/migrations/043_view_invoker.sql +0 -90
- package/migrations/044_unit_member_facts.sql +0 -47
- package/migrations/045_unit_entry.sql +0 -236
- package/migrations/046_membership_visible_to_members.sql +0 -85
- package/migrations/047_tasks.sql +0 -266
- package/migrations/048_every_login_is_a_person.sql +0 -190
- package/migrations/049_bookable_people.sql +0 -126
- package/migrations/050_authz_core.sql +0 -230
- package/migrations/051_authz_helpers.sql +0 -330
- package/migrations/052_authz_fanout.sql +0 -253
- package/migrations/053_authz_catalog_templates.sql +0 -402
- package/migrations/060_tenancy_seam.sql +0 -132
- package/migrations/061_create_tenant.sql +0 -125
- package/migrations/062_invitations.sql +0 -329
- package/migrations/063_legacy_tenancy_retired.sql +0 -568
- package/migrations/064_agent_guard_authz.sql +0 -118
- package/migrations/064b_supersede_unit_scope.sql +0 -205
- package/migrations/065_scaffold_sweep.sql +0 -139
- package/migrations/066_shadow_report.sql +0 -122
- package/migrations/070_permission_catalog.sql +0 -205
- package/migrations/071_v1_permission_map.sql +0 -266
- package/migrations/072_role_rpcs.sql +0 -126
- package/migrations/073_legacy_rbac_retire.sql +0 -471
- package/migrations/075_company_legal_entities.sql +0 -169
- package/migrations/076_tenant_settings.sql +0 -251
- package/migrations/080_people_roles.sql +0 -269
- package/migrations/081_people_identity.sql +0 -271
- package/migrations/082_people_membership_link.sql +0 -142
- package/migrations/083_people_data_migration.sql +0 -75
- package/migrations/084_people_registrations_tokens.sql +0 -282
- package/migrations/085_catalog_products.sql +0 -333
- package/migrations/086_catalog_pricing.sql +0 -194
- package/migrations/087_catalog_packages.sql +0 -134
- package/migrations/090_orders_venda.sql +0 -166
- package/migrations/091_orders_scaffold.sql +0 -92
- package/migrations/095_config_field_rules.sql +0 -261
- package/migrations/096_audit_append_only.sql +0 -256
- package/migrations/097_sequences_authorized.sql +0 -111
- package/migrations/098_holidays.sql +0 -80
- package/migrations/099_access_grants.sql +0 -293
- package/migrations/100_migration_schema.sql +0 -217
- package/migrations/101_migration_allowlist.sql +0 -202
- package/migrations/102_migration_rpcs.sql +0 -933
- package/migrations/103_migration_reconcile_ponr.sql +0 -132
- package/migrations/105_policies_to_authenticated.sql +0 -71
- package/migrations/106_function_search_path.sql +0 -55
- package/migrations/107_core_table_prefix.sql +0 -361
- package/migrations/108_authz_write_guards.sql +0 -149
- package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
- package/migrations/110_people_kind_vocabulary.sql +0 -120
- package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
- package/migrations/112_money_precision_standard.sql +0 -132
- package/migrations/113_addresses_units_companies.sql +0 -114
- package/migrations/114_migrations_ledger_immutable.sql +0 -65
- package/migrations/115_registration_honours_field_rules.sql +0 -172
- package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
- package/migrations/117_authz_reconciliation_schedule.sql +0 -82
- package/migrations/118_migration_catalog_writers.sql +0 -450
- package/migrations/119_migration_config_writers.sql +0 -193
- package/migrations/120_migration_facet_writer.sql +0 -139
- package/migrations/121_migration_financial_writers.sql +0 -294
- package/migrations/122_migration_inventory_writers.sql +0 -245
- package/migrations/123_migration_order_item_writer.sql +0 -144
- package/migrations/124_migration_ledger_writers.sql +0 -170
- package/migrations/125_migration_authz_people_writers.sql +0 -205
- package/migrations/126_owner_scoping_is_configuration.sql +0 -247
- package/migrations/127_scaffold_audit_hook.sql +0 -98
- package/migrations/128_shadow_tables_enforced.sql +0 -93
- package/migrations/129_order_kind_terminal_states.sql +0 -57
- package/migrations/130_every_login_is_a_person.sql +0 -196
- package/migrations/131_migration_invoice_split.sql +0 -127
- package/migrations/132_audit_prune_scheduled.sql +0 -28
- package/migrations/133_unit_is_a_session_property.sql +0 -155
- package/migrations/134_field_rules_on_read.sql +0 -341
- package/migrations/135_distribution_polarity.sql +0 -158
- package/migrations/136_unit_id_means_locality.sql +0 -358
- package/migrations/137_vendor_named_spine_objects.sql +0 -126
- package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
- package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
- package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
- package/migrations/142_a_person_has_addresses.sql +0 -212
- package/migrations/143_a_professional_has_a_rate.sql +0 -75
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
- package/migrations/146_products_stock_has_one_writer.sql +0 -67
- package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
- package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
- package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
- package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
- package/migrations/152_a_file_is_an_archetype.sql +0 -222
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
- package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
- package/migrations/155_a_tenant_can_leave.sql +0 -271
- package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
- package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
- package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
- package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
- package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
- package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
- package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
- package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 050_authz_core.sql — the authz core (ADR 0006 / PRD 01 / #133): schema §3.
|
|
3
|
-
--
|
|
4
|
-
-- Tenancy, RBAC catalog, membership, the precomputed unit scope and the generic
|
|
5
|
-
-- relation tuples of the Multi-Tenant Authorization & Data Scoping
|
|
6
|
-
-- Architecture v2. Exactly two authorization surfaces (user_unit_access,
|
|
7
|
-
-- resource_grants) plus the RBAC catalog; business tables carry tenant_id /
|
|
8
|
-
-- unit_id / owner_id and one RLS template (043) — nothing else decides access.
|
|
9
|
-
--
|
|
10
|
-
-- Where it lives (DECISIONS 2026-08-18): schema `app`, per the spec and the
|
|
11
|
-
-- issue's default until the platform session rules otherwise. `public.tenants`
|
|
12
|
-
-- stays the tenant table — every FK in the fleet already points at it — and
|
|
13
|
-
-- gains `kind`. Reads for UI go through public.v_authz_* (043); writes through
|
|
14
|
-
-- the RLS write policies below or the RPCs of #134.
|
|
15
|
-
--
|
|
16
|
-
-- Idempotent and replay-safe: IF NOT EXISTS / guarded DO blocks throughout;
|
|
17
|
-
-- policies are DROP IF EXISTS + CREATE. Nothing here touches tenant_members
|
|
18
|
-
-- or locations — retiring them is #134's migration, on top of this.
|
|
19
|
-
-- ============================================================================
|
|
20
|
-
|
|
21
|
-
CREATE SCHEMA IF NOT EXISTS app;
|
|
22
|
-
GRANT USAGE ON SCHEMA app TO authenticated, service_role;
|
|
23
|
-
-- anon deliberately gets nothing: the authz core has no anonymous surface.
|
|
24
|
-
|
|
25
|
-
-- ─────────────────────────────── Tenancy ───────────────────────────────
|
|
26
|
-
-- tenants(kind network|business): a network groups units under franchisee /
|
|
27
|
-
-- regional parent units; a business is one company. ADR 0002 maps V1's licence
|
|
28
|
-
-- to a tenant and V1's franchise mode to kind='network'.
|
|
29
|
-
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT 'business';
|
|
30
|
-
DO $$
|
|
31
|
-
BEGIN
|
|
32
|
-
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tenants_kind_check' AND conrelid = 'public.tenants'::regclass) THEN
|
|
33
|
-
ALTER TABLE public.tenants ADD CONSTRAINT tenants_kind_check CHECK (kind IN ('network', 'business'));
|
|
34
|
-
END IF;
|
|
35
|
-
END $$;
|
|
36
|
-
|
|
37
|
-
CREATE TABLE IF NOT EXISTS app.units (
|
|
38
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
39
|
-
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
40
|
-
kind text NOT NULL CHECK (kind IN ('hq', 'branch')),
|
|
41
|
-
parent_unit_id uuid, -- grouping: franchisee, region (subtree propagation)
|
|
42
|
-
name text NOT NULL,
|
|
43
|
-
slug text,
|
|
44
|
-
active boolean NOT NULL DEFAULT true,
|
|
45
|
-
company_legal_entity_id uuid, -- Empresa: public.company_legal_entities (CNPJ/CPF); FK added by 065 (#138)
|
|
46
|
-
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
47
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
48
|
-
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
49
|
-
PRIMARY KEY (id),
|
|
50
|
-
UNIQUE (tenant_id, id),
|
|
51
|
-
UNIQUE (tenant_id, slug),
|
|
52
|
-
FOREIGN KEY (tenant_id, parent_unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL,
|
|
53
|
-
CHECK (parent_unit_id IS DISTINCT FROM id)
|
|
54
|
-
);
|
|
55
|
-
CREATE INDEX IF NOT EXISTS units_tenant_parent_idx ON app.units (tenant_id, parent_unit_id);
|
|
56
|
-
CREATE UNIQUE INDEX IF NOT EXISTS units_one_hq_per_tenant ON app.units (tenant_id) WHERE kind = 'hq';
|
|
57
|
-
DROP TRIGGER IF EXISTS units_updated_at ON app.units;
|
|
58
|
-
CREATE TRIGGER units_updated_at BEFORE UPDATE ON app.units
|
|
59
|
-
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
60
|
-
|
|
61
|
-
-- ─────────────────────────────── RBAC catalog ──────────────────────────
|
|
62
|
-
-- Global catalog: keys are '{plugin}.{action}', seeded by the platform and by
|
|
63
|
-
-- each plugin's declaredFeatures (#136). A key is a row, never DDL.
|
|
64
|
-
CREATE TABLE IF NOT EXISTS app.permissions (
|
|
65
|
-
key text PRIMARY KEY,
|
|
66
|
-
plugin text NOT NULL,
|
|
67
|
-
description text,
|
|
68
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
69
|
-
CHECK (key ~ '^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_-]*)+$')
|
|
70
|
-
);
|
|
71
|
-
CREATE INDEX IF NOT EXISTS permissions_plugin_idx ON app.permissions (plugin);
|
|
72
|
-
|
|
73
|
-
-- Roles per tenant. is_system rows come from the vertical template (owner,
|
|
74
|
-
-- manager, reception, professional, …) and cannot be deleted; a tenant may add
|
|
75
|
-
-- its own ("kit por cargo" of a network is a tenant role, not a system one).
|
|
76
|
-
CREATE TABLE IF NOT EXISTS app.roles (
|
|
77
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
78
|
-
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
79
|
-
key text NOT NULL,
|
|
80
|
-
name text NOT NULL,
|
|
81
|
-
description text,
|
|
82
|
-
is_system boolean NOT NULL DEFAULT false,
|
|
83
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
84
|
-
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
85
|
-
PRIMARY KEY (tenant_id, id),
|
|
86
|
-
UNIQUE (id),
|
|
87
|
-
UNIQUE (tenant_id, key),
|
|
88
|
-
CHECK (key ~ '^[a-z][a-z0-9_-]*$')
|
|
89
|
-
);
|
|
90
|
-
DROP TRIGGER IF EXISTS roles_updated_at ON app.roles;
|
|
91
|
-
CREATE TRIGGER roles_updated_at BEFORE UPDATE ON app.roles
|
|
92
|
-
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
93
|
-
|
|
94
|
-
CREATE TABLE IF NOT EXISTS app.role_permissions (
|
|
95
|
-
tenant_id uuid NOT NULL,
|
|
96
|
-
role_id uuid NOT NULL,
|
|
97
|
-
permission text NOT NULL REFERENCES app.permissions(key) ON DELETE CASCADE,
|
|
98
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
99
|
-
PRIMARY KEY (tenant_id, role_id, permission),
|
|
100
|
-
FOREIGN KEY (tenant_id, role_id) REFERENCES app.roles(tenant_id, id) ON DELETE CASCADE
|
|
101
|
-
);
|
|
102
|
-
CREATE INDEX IF NOT EXISTS role_permissions_perm_idx ON app.role_permissions (tenant_id, permission);
|
|
103
|
-
|
|
104
|
-
-- ─────────────────────────────── Membership ────────────────────────────
|
|
105
|
-
-- One membership per (tenant, user): the base role, whether the user sees every
|
|
106
|
-
-- unit (franchisor / network owner), and whether it is active. person_id links
|
|
107
|
-
-- the login to the one Person of that human in the tenant (#137).
|
|
108
|
-
CREATE TABLE IF NOT EXISTS app.memberships (
|
|
109
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
110
|
-
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
111
|
-
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
112
|
-
role_id uuid NOT NULL,
|
|
113
|
-
all_units boolean NOT NULL DEFAULT false,
|
|
114
|
-
active boolean NOT NULL DEFAULT true,
|
|
115
|
-
person_id uuid,
|
|
116
|
-
invited_by uuid,
|
|
117
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
118
|
-
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
119
|
-
PRIMARY KEY (tenant_id, user_id),
|
|
120
|
-
UNIQUE (id),
|
|
121
|
-
FOREIGN KEY (tenant_id, role_id) REFERENCES app.roles(tenant_id, id)
|
|
122
|
-
);
|
|
123
|
-
CREATE INDEX IF NOT EXISTS memberships_user_idx ON app.memberships (user_id) WHERE active;
|
|
124
|
-
CREATE INDEX IF NOT EXISTS memberships_role_idx ON app.memberships (tenant_id, role_id);
|
|
125
|
-
DROP TRIGGER IF EXISTS memberships_updated_at ON app.memberships;
|
|
126
|
-
CREATE TRIGGER memberships_updated_at BEFORE UPDATE ON app.memberships
|
|
127
|
-
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
128
|
-
|
|
129
|
-
-- Explicit units of a member (Toast/Square style) with an optional per-unit
|
|
130
|
-
-- role override — the effective role in that unit (§8).
|
|
131
|
-
CREATE TABLE IF NOT EXISTS app.membership_units (
|
|
132
|
-
tenant_id uuid NOT NULL,
|
|
133
|
-
user_id uuid NOT NULL,
|
|
134
|
-
unit_id uuid NOT NULL,
|
|
135
|
-
role_override_id uuid,
|
|
136
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
137
|
-
PRIMARY KEY (tenant_id, user_id, unit_id),
|
|
138
|
-
FOREIGN KEY (tenant_id, user_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE CASCADE,
|
|
139
|
-
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE,
|
|
140
|
-
FOREIGN KEY (tenant_id, role_override_id) REFERENCES app.roles(tenant_id, id)
|
|
141
|
-
);
|
|
142
|
-
CREATE INDEX IF NOT EXISTS membership_units_unit_idx ON app.membership_units (tenant_id, unit_id);
|
|
143
|
-
|
|
144
|
-
-- ─────────────── Precomputed scope (Salesforce-style share table) ───────
|
|
145
|
-
-- DERIVED. Maintained by the fan-out triggers (042) and the reconciliation job.
|
|
146
|
-
-- Never written by app code: no INSERT/UPDATE/DELETE grant to any API role.
|
|
147
|
-
CREATE TABLE IF NOT EXISTS app.user_unit_access (
|
|
148
|
-
tenant_id uuid NOT NULL,
|
|
149
|
-
user_id uuid NOT NULL,
|
|
150
|
-
unit_id uuid NOT NULL,
|
|
151
|
-
via text NOT NULL CHECK (via IN ('all_units', 'explicit', 'subtree')),
|
|
152
|
-
PRIMARY KEY (tenant_id, user_id, unit_id),
|
|
153
|
-
FOREIGN KEY (tenant_id, user_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE CASCADE,
|
|
154
|
-
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE
|
|
155
|
-
);
|
|
156
|
-
CREATE INDEX IF NOT EXISTS user_unit_access_unit_idx ON app.user_unit_access (tenant_id, unit_id);
|
|
157
|
-
|
|
158
|
-
-- Diff-log of every reconciliation run (§6). diff <> 0 outside a migration is
|
|
159
|
-
-- an incident, not a perf issue: v_authz_reconciliation_alerts (043) lists them.
|
|
160
|
-
CREATE TABLE IF NOT EXISTS app.authz_reconciliation_log (
|
|
161
|
-
id bigserial PRIMARY KEY,
|
|
162
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
163
|
-
run_at timestamptz NOT NULL DEFAULT now(),
|
|
164
|
-
source text NOT NULL CHECK (source IN ('migration', 'scheduled', 'manual', 'test')),
|
|
165
|
-
added integer NOT NULL DEFAULT 0,
|
|
166
|
-
removed integer NOT NULL DEFAULT 0,
|
|
167
|
-
details jsonb NOT NULL DEFAULT '[]'::jsonb
|
|
168
|
-
);
|
|
169
|
-
CREATE INDEX IF NOT EXISTS authz_reconciliation_log_tenant_idx ON app.authz_reconciliation_log (tenant_id, run_at DESC);
|
|
170
|
-
|
|
171
|
-
-- ─────────────── Generic relation tuples (ERPNext UserPermission / Zanzibar) ───
|
|
172
|
-
CREATE TABLE IF NOT EXISTS app.resource_grants (
|
|
173
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
174
|
-
resource_type text NOT NULL, -- plugin-namespaced: 'catalog.product', 'people.person', …
|
|
175
|
-
resource_id uuid NOT NULL,
|
|
176
|
-
relation text NOT NULL, -- 'available_in', 'owner', 'shared_with', 'visible_to', …
|
|
177
|
-
subject_type text NOT NULL CHECK (subject_type IN ('user', 'unit', 'role')),
|
|
178
|
-
subject_id uuid NOT NULL,
|
|
179
|
-
created_by uuid,
|
|
180
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
181
|
-
PRIMARY KEY (tenant_id, resource_type, resource_id, relation, subject_type, subject_id),
|
|
182
|
-
CHECK (resource_type ~ '^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$'),
|
|
183
|
-
CHECK (relation ~ '^[a-z][a-z0-9_]*$')
|
|
184
|
-
);
|
|
185
|
-
-- Reverse lookup: "what can this subject access?"
|
|
186
|
-
CREATE INDEX IF NOT EXISTS resource_grants_subject_idx
|
|
187
|
-
ON app.resource_grants (tenant_id, subject_type, subject_id, relation, resource_type);
|
|
188
|
-
|
|
189
|
-
-- ─────────────────────────────── Grants ────────────────────────────────
|
|
190
|
-
-- Reads for every API role that has a session; RLS (below) narrows to the
|
|
191
|
-
-- caller's tenant. Writes only where a policy exists to gate them.
|
|
192
|
-
GRANT SELECT ON app.units, app.permissions, app.roles, app.role_permissions,
|
|
193
|
-
app.memberships, app.membership_units, app.user_unit_access,
|
|
194
|
-
app.resource_grants, app.authz_reconciliation_log TO authenticated;
|
|
195
|
-
GRANT INSERT, UPDATE, DELETE ON app.units, app.roles, app.role_permissions,
|
|
196
|
-
app.memberships, app.membership_units, app.resource_grants TO authenticated;
|
|
197
|
-
GRANT ALL ON ALL TABLES IN SCHEMA app TO service_role;
|
|
198
|
-
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO service_role;
|
|
199
|
-
-- user_unit_access is derived: no API role may write it (the trigger functions
|
|
200
|
-
-- are SECURITY DEFINER and owned by the migration role).
|
|
201
|
-
REVOKE INSERT, UPDATE, DELETE ON app.user_unit_access FROM authenticated, anon;
|
|
202
|
-
REVOKE ALL ON app.permissions FROM anon;
|
|
203
|
-
REVOKE INSERT, UPDATE, DELETE ON app.permissions FROM authenticated;
|
|
204
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA app REVOKE ALL ON TABLES FROM anon;
|
|
205
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA app REVOKE EXECUTE ON FUNCTIONS FROM public, anon;
|
|
206
|
-
|
|
207
|
-
-- ─────────────────────────────── RLS on the authz tables ────────────────
|
|
208
|
-
-- The helpers of 041 are needed by the policies below; 041 runs after this file,
|
|
209
|
-
-- so the policies are (re)created at the end of 041 where the functions exist.
|
|
210
|
-
ALTER TABLE app.units ENABLE ROW LEVEL SECURITY;
|
|
211
|
-
ALTER TABLE app.units FORCE ROW LEVEL SECURITY;
|
|
212
|
-
ALTER TABLE app.permissions ENABLE ROW LEVEL SECURITY;
|
|
213
|
-
ALTER TABLE app.roles ENABLE ROW LEVEL SECURITY;
|
|
214
|
-
ALTER TABLE app.roles FORCE ROW LEVEL SECURITY;
|
|
215
|
-
ALTER TABLE app.role_permissions ENABLE ROW LEVEL SECURITY;
|
|
216
|
-
ALTER TABLE app.role_permissions FORCE ROW LEVEL SECURITY;
|
|
217
|
-
ALTER TABLE app.memberships ENABLE ROW LEVEL SECURITY;
|
|
218
|
-
ALTER TABLE app.memberships FORCE ROW LEVEL SECURITY;
|
|
219
|
-
ALTER TABLE app.membership_units ENABLE ROW LEVEL SECURITY;
|
|
220
|
-
ALTER TABLE app.membership_units FORCE ROW LEVEL SECURITY;
|
|
221
|
-
ALTER TABLE app.user_unit_access ENABLE ROW LEVEL SECURITY;
|
|
222
|
-
ALTER TABLE app.user_unit_access FORCE ROW LEVEL SECURITY;
|
|
223
|
-
ALTER TABLE app.resource_grants ENABLE ROW LEVEL SECURITY;
|
|
224
|
-
ALTER TABLE app.resource_grants FORCE ROW LEVEL SECURITY;
|
|
225
|
-
ALTER TABLE app.authz_reconciliation_log ENABLE ROW LEVEL SECURITY;
|
|
226
|
-
ALTER TABLE app.authz_reconciliation_log FORCE ROW LEVEL SECURITY;
|
|
227
|
-
|
|
228
|
-
-- The catalog is global and harmless to read: every signed-in user may list it.
|
|
229
|
-
DROP POLICY IF EXISTS permissions_select ON app.permissions;
|
|
230
|
-
CREATE POLICY permissions_select ON app.permissions FOR SELECT TO authenticated USING (true);
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 051_authz_helpers.sql — the RLS helpers (§7) and effective role (§8).
|
|
3
|
-
--
|
|
4
|
-
-- All `STABLE SECURITY DEFINER SET search_path = ''`, meant to be called wrapped
|
|
5
|
-
-- in `(select …)` so the planner caches them per statement (initPlan). They
|
|
6
|
-
-- read the authz tables with the migration role's privileges (no recursion into
|
|
7
|
-
-- RLS) and never raise: an RLS policy is not a place to throw, so junk resolves
|
|
8
|
-
-- to "no access".
|
|
9
|
-
--
|
|
10
|
-
-- Also here: the RLS policies of the authz tables themselves (040 could not
|
|
11
|
-
-- create them before the helpers existed).
|
|
12
|
-
-- ============================================================================
|
|
13
|
-
|
|
14
|
-
-- ── current_tenant_id() ─────────────────────────────────────────────────
|
|
15
|
-
-- The tenant this request acts in. Same claim semantics as public.user_tenant_ids()
|
|
16
|
-
-- (028): a top-level `tenant_id` JWT claim NARROWS and never widens — with a
|
|
17
|
-
-- signed-in session it must match an active membership; a session-less signed
|
|
18
|
-
-- token is scoped by the claim alone (only we can mint one). Without a claim,
|
|
19
|
-
-- a user with exactly one active membership acts in it; a multi-tenant user
|
|
20
|
-
-- without a claim acts in none (fail closed — the app sets the claim / active
|
|
21
|
-
-- tenant). Only tenant_id (and, later, the active unit) live in the JWT:
|
|
22
|
-
-- permissions and unit scope always come from tables (revocation is instant).
|
|
23
|
-
CREATE OR REPLACE FUNCTION app.current_tenant_id()
|
|
24
|
-
RETURNS uuid
|
|
25
|
-
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
26
|
-
SET search_path = ''
|
|
27
|
-
AS $$
|
|
28
|
-
DECLARE
|
|
29
|
-
v_uid uuid := auth.uid();
|
|
30
|
-
v_jwt jsonb := coalesce(auth.jwt(), '{}'::jsonb);
|
|
31
|
-
v_raw text;
|
|
32
|
-
v_tenant uuid;
|
|
33
|
-
v_n integer;
|
|
34
|
-
BEGIN
|
|
35
|
-
IF jsonb_exists(v_jwt, 'tenant_id') THEN
|
|
36
|
-
v_raw := v_jwt ->> 'tenant_id';
|
|
37
|
-
-- Presence expresses intent to be narrowed; an unparseable value is no tenant.
|
|
38
|
-
IF v_raw IS NULL OR v_raw !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN
|
|
39
|
-
RETURN NULL;
|
|
40
|
-
END IF;
|
|
41
|
-
v_tenant := v_raw::uuid;
|
|
42
|
-
IF v_uid IS NULL THEN
|
|
43
|
-
RETURN v_tenant;
|
|
44
|
-
END IF;
|
|
45
|
-
IF EXISTS (SELECT 1 FROM app.memberships m WHERE m.tenant_id = v_tenant AND m.user_id = v_uid AND m.active) THEN
|
|
46
|
-
RETURN v_tenant;
|
|
47
|
-
END IF;
|
|
48
|
-
RETURN NULL;
|
|
49
|
-
END IF;
|
|
50
|
-
|
|
51
|
-
IF v_uid IS NULL THEN
|
|
52
|
-
RETURN NULL;
|
|
53
|
-
END IF;
|
|
54
|
-
SELECT count(*), (array_agg(m.tenant_id))[1] INTO v_n, v_tenant
|
|
55
|
-
FROM app.memberships m WHERE m.user_id = v_uid AND m.active;
|
|
56
|
-
IF v_n = 1 THEN
|
|
57
|
-
RETURN v_tenant;
|
|
58
|
-
END IF;
|
|
59
|
-
RETURN NULL;
|
|
60
|
-
END $$;
|
|
61
|
-
|
|
62
|
-
-- ── unit_scoping_on() — the per-account switch main already had ─────────
|
|
63
|
-
-- `039_unit_tree` gave every tenant a `unit_scoping_enabled`, off by default,
|
|
64
|
-
-- and that is a better design than the always-on this chain originally shipped:
|
|
65
|
-
-- a network turns it on when it is ready, and turning it off gives everything
|
|
66
|
-
-- back without moving a row. The policy template honours that switch instead of
|
|
67
|
-
-- replacing it.
|
|
68
|
-
--
|
|
69
|
-
-- Switch off ⇒ every unit of the account is yours, which is exactly the
|
|
70
|
-
-- behaviour from before units existed.
|
|
71
|
-
CREATE OR REPLACE FUNCTION app.unit_scoping_on()
|
|
72
|
-
RETURNS boolean
|
|
73
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
74
|
-
SET search_path = ''
|
|
75
|
-
AS $$
|
|
76
|
-
SELECT coalesce((SELECT t.unit_scoping_enabled FROM public.tenants t
|
|
77
|
-
WHERE t.id = app.current_tenant_id()), false);
|
|
78
|
-
$$;
|
|
79
|
-
REVOKE ALL ON FUNCTION app.unit_scoping_on() FROM public, anon;
|
|
80
|
-
GRANT EXECUTE ON FUNCTION app.unit_scoping_on() TO authenticated, service_role;
|
|
81
|
-
|
|
82
|
-
-- ── has_unit(unit) ──────────────────────────────────────────────────────
|
|
83
|
-
-- O(1) on the share table's PK. A NULL unit is a tenant-wide resource: the
|
|
84
|
-
-- template checks `unit_id IS NULL OR has_unit(unit_id)` itself.
|
|
85
|
-
CREATE OR REPLACE FUNCTION app.has_unit(p_unit uuid)
|
|
86
|
-
RETURNS boolean
|
|
87
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
88
|
-
SET search_path = ''
|
|
89
|
-
AS $$
|
|
90
|
-
SELECT p_unit IS NOT NULL
|
|
91
|
-
AND CASE WHEN app.unit_scoping_on()
|
|
92
|
-
THEN EXISTS (SELECT 1 FROM app.user_unit_access a
|
|
93
|
-
WHERE a.tenant_id = app.current_tenant_id()
|
|
94
|
-
AND a.user_id = auth.uid()
|
|
95
|
-
AND a.unit_id = p_unit)
|
|
96
|
-
-- switch off: every unit of the account is yours
|
|
97
|
-
ELSE EXISTS (SELECT 1 FROM app.units u
|
|
98
|
-
WHERE u.id = p_unit AND u.tenant_id = app.current_tenant_id())
|
|
99
|
-
END;
|
|
100
|
-
$$;
|
|
101
|
-
|
|
102
|
-
-- ── effective_role_id(tenant, user, unit) ───────────────────────────────
|
|
103
|
-
-- §8: membership_units.role_override_id when present for (user, unit), else
|
|
104
|
-
-- memberships.role_id. Inactive membership ⇒ no role.
|
|
105
|
-
CREATE OR REPLACE FUNCTION app.effective_role_id(p_tenant uuid, p_user uuid, p_unit uuid DEFAULT NULL)
|
|
106
|
-
RETURNS uuid
|
|
107
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
108
|
-
SET search_path = ''
|
|
109
|
-
AS $$
|
|
110
|
-
SELECT coalesce(
|
|
111
|
-
(SELECT mu.role_override_id
|
|
112
|
-
FROM app.membership_units mu
|
|
113
|
-
WHERE p_unit IS NOT NULL
|
|
114
|
-
AND mu.tenant_id = p_tenant AND mu.user_id = p_user AND mu.unit_id = p_unit
|
|
115
|
-
AND mu.role_override_id IS NOT NULL),
|
|
116
|
-
(SELECT m.role_id
|
|
117
|
-
FROM app.memberships m
|
|
118
|
-
WHERE m.tenant_id = p_tenant AND m.user_id = p_user AND m.active)
|
|
119
|
-
);
|
|
120
|
-
$$;
|
|
121
|
-
|
|
122
|
-
-- ── has_permission(perm[, unit]) ────────────────────────────────────────
|
|
123
|
-
-- Resolves against the effective role for the row's unit when one is given,
|
|
124
|
-
-- else the base role. `{feature}.manage` implies every `{feature}.{action}`
|
|
125
|
-
-- (the SDK's resolveAccess semantics, mirrored — same source, no drift). No
|
|
126
|
-
-- owner bypass: an owner is a role whose permission set is the whole catalog.
|
|
127
|
-
CREATE OR REPLACE FUNCTION app.has_permission(p_perm text, p_unit uuid DEFAULT NULL)
|
|
128
|
-
RETURNS boolean
|
|
129
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
130
|
-
SET search_path = ''
|
|
131
|
-
AS $$
|
|
132
|
-
SELECT EXISTS (
|
|
133
|
-
SELECT 1
|
|
134
|
-
FROM app.role_permissions rp
|
|
135
|
-
WHERE rp.tenant_id = app.current_tenant_id()
|
|
136
|
-
AND rp.role_id = app.effective_role_id(app.current_tenant_id(), auth.uid(), p_unit)
|
|
137
|
-
AND rp.permission IN (p_perm, regexp_replace(p_perm, '\.[^.]+$', '.manage'))
|
|
138
|
-
);
|
|
139
|
-
$$;
|
|
140
|
-
|
|
141
|
-
-- Does the user hold the permission in ANY of their roles (base or any unit
|
|
142
|
-
-- override)? For UI affordances and tenant-wide actions, not for row scope.
|
|
143
|
-
CREATE OR REPLACE FUNCTION app.has_permission_anywhere(p_perm text)
|
|
144
|
-
RETURNS boolean
|
|
145
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
146
|
-
SET search_path = ''
|
|
147
|
-
AS $$
|
|
148
|
-
SELECT EXISTS (
|
|
149
|
-
SELECT 1
|
|
150
|
-
FROM app.role_permissions rp
|
|
151
|
-
WHERE rp.tenant_id = app.current_tenant_id()
|
|
152
|
-
AND rp.permission IN (p_perm, regexp_replace(p_perm, '\.[^.]+$', '.manage'))
|
|
153
|
-
AND rp.role_id IN (
|
|
154
|
-
SELECT m.role_id FROM app.memberships m
|
|
155
|
-
WHERE m.tenant_id = rp.tenant_id AND m.user_id = auth.uid() AND m.active
|
|
156
|
-
UNION
|
|
157
|
-
SELECT mu.role_override_id FROM app.membership_units mu
|
|
158
|
-
WHERE mu.tenant_id = rp.tenant_id AND mu.user_id = auth.uid() AND mu.role_override_id IS NOT NULL
|
|
159
|
-
)
|
|
160
|
-
);
|
|
161
|
-
$$;
|
|
162
|
-
|
|
163
|
-
-- ── has_grant(type, id, relations[]) ────────────────────────────────────
|
|
164
|
-
-- resource_grants where the subject is me, one of my units, or one of my roles.
|
|
165
|
-
CREATE OR REPLACE FUNCTION app.has_grant(p_type text, p_id uuid, p_relations text[])
|
|
166
|
-
RETURNS boolean
|
|
167
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
168
|
-
SET search_path = ''
|
|
169
|
-
AS $$
|
|
170
|
-
SELECT EXISTS (
|
|
171
|
-
SELECT 1
|
|
172
|
-
FROM app.resource_grants g
|
|
173
|
-
WHERE g.tenant_id = app.current_tenant_id()
|
|
174
|
-
AND g.resource_type = p_type
|
|
175
|
-
AND g.resource_id = p_id
|
|
176
|
-
AND g.relation = ANY (p_relations)
|
|
177
|
-
AND (
|
|
178
|
-
(g.subject_type = 'user' AND g.subject_id = auth.uid())
|
|
179
|
-
OR (g.subject_type = 'unit' AND EXISTS (
|
|
180
|
-
SELECT 1 FROM app.user_unit_access a
|
|
181
|
-
WHERE a.tenant_id = g.tenant_id AND a.user_id = auth.uid() AND a.unit_id = g.subject_id))
|
|
182
|
-
OR (g.subject_type = 'role' AND g.subject_id IN (
|
|
183
|
-
SELECT m.role_id FROM app.memberships m
|
|
184
|
-
WHERE m.tenant_id = g.tenant_id AND m.user_id = auth.uid() AND m.active
|
|
185
|
-
UNION
|
|
186
|
-
SELECT mu.role_override_id FROM app.membership_units mu
|
|
187
|
-
WHERE mu.tenant_id = g.tenant_id AND mu.user_id = auth.uid() AND mu.role_override_id IS NOT NULL))
|
|
188
|
-
)
|
|
189
|
-
);
|
|
190
|
-
$$;
|
|
191
|
-
|
|
192
|
-
-- "No available_in tuples ⇒ available everywhere" (§9.1): a tenant-wide
|
|
193
|
-
-- resource is visible in a unit unless the resource has been restricted, in
|
|
194
|
-
-- which case the unit must be one of the granted ones.
|
|
195
|
-
CREATE OR REPLACE FUNCTION app.available_in_my_units(p_type text, p_id uuid)
|
|
196
|
-
RETURNS boolean
|
|
197
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
198
|
-
SET search_path = ''
|
|
199
|
-
AS $$
|
|
200
|
-
SELECT NOT EXISTS (
|
|
201
|
-
SELECT 1 FROM app.resource_grants g
|
|
202
|
-
WHERE g.tenant_id = app.current_tenant_id()
|
|
203
|
-
AND g.resource_type = p_type AND g.resource_id = p_id AND g.relation = 'available_in')
|
|
204
|
-
OR app.has_grant(p_type, p_id, ARRAY['available_in']);
|
|
205
|
-
$$;
|
|
206
|
-
|
|
207
|
-
-- ── is_member(tenant) ───────────────────────────────────────────────────
|
|
208
|
-
-- Plain active membership, no permission — what the authz tables' own read
|
|
209
|
-
-- policies need ("members see their tenant's units and roles").
|
|
210
|
-
CREATE OR REPLACE FUNCTION app.is_member(p_tenant uuid)
|
|
211
|
-
RETURNS boolean
|
|
212
|
-
LANGUAGE sql STABLE SECURITY DEFINER
|
|
213
|
-
SET search_path = ''
|
|
214
|
-
AS $$
|
|
215
|
-
SELECT EXISTS (
|
|
216
|
-
SELECT 1 FROM app.memberships m
|
|
217
|
-
WHERE m.tenant_id = p_tenant AND m.user_id = auth.uid() AND m.active
|
|
218
|
-
);
|
|
219
|
-
$$;
|
|
220
|
-
|
|
221
|
-
REVOKE ALL ON FUNCTION app.current_tenant_id() FROM public;
|
|
222
|
-
REVOKE ALL ON FUNCTION app.has_unit(uuid) FROM public;
|
|
223
|
-
REVOKE ALL ON FUNCTION app.effective_role_id(uuid, uuid, uuid) FROM public;
|
|
224
|
-
REVOKE ALL ON FUNCTION app.has_permission(text, uuid) FROM public;
|
|
225
|
-
REVOKE ALL ON FUNCTION app.has_permission_anywhere(text) FROM public;
|
|
226
|
-
REVOKE ALL ON FUNCTION app.has_grant(text, uuid, text[]) FROM public;
|
|
227
|
-
REVOKE ALL ON FUNCTION app.available_in_my_units(text, uuid) FROM public;
|
|
228
|
-
REVOKE ALL ON FUNCTION app.is_member(uuid) FROM public;
|
|
229
|
-
GRANT EXECUTE ON FUNCTION app.current_tenant_id() TO authenticated, service_role;
|
|
230
|
-
GRANT EXECUTE ON FUNCTION app.has_unit(uuid) TO authenticated, service_role;
|
|
231
|
-
GRANT EXECUTE ON FUNCTION app.effective_role_id(uuid, uuid, uuid) TO authenticated, service_role;
|
|
232
|
-
GRANT EXECUTE ON FUNCTION app.has_permission(text, uuid) TO authenticated, service_role;
|
|
233
|
-
GRANT EXECUTE ON FUNCTION app.has_permission_anywhere(text) TO authenticated, service_role;
|
|
234
|
-
GRANT EXECUTE ON FUNCTION app.has_grant(text, uuid, text[]) TO authenticated, service_role;
|
|
235
|
-
GRANT EXECUTE ON FUNCTION app.available_in_my_units(text, uuid) TO authenticated, service_role;
|
|
236
|
-
GRANT EXECUTE ON FUNCTION app.is_member(uuid) TO authenticated, service_role;
|
|
237
|
-
|
|
238
|
-
-- ═══════════════════════ RLS on the authz tables ══════════════════════════
|
|
239
|
-
-- Reads: any active member of the tenant sees the tenant's units, roles and
|
|
240
|
-
-- role permissions (the UI needs them to render); memberships and unit rows are
|
|
241
|
-
-- visible to the member herself and to holders of members.read; the share table
|
|
242
|
-
-- to the user herself and to members.read; grants to members of the tenant.
|
|
243
|
-
-- Writes: platform permission keys — no owner/admin literal anywhere.
|
|
244
|
-
|
|
245
|
-
-- units
|
|
246
|
-
DROP POLICY IF EXISTS units_select ON app.units;
|
|
247
|
-
CREATE POLICY units_select ON app.units FOR SELECT TO authenticated
|
|
248
|
-
USING ((select app.is_member(tenant_id)));
|
|
249
|
-
DROP POLICY IF EXISTS units_insert ON app.units;
|
|
250
|
-
CREATE POLICY units_insert ON app.units FOR INSERT TO authenticated
|
|
251
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('units.manage')));
|
|
252
|
-
DROP POLICY IF EXISTS units_update ON app.units;
|
|
253
|
-
CREATE POLICY units_update ON app.units FOR UPDATE TO authenticated
|
|
254
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('units.manage')))
|
|
255
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('units.manage')));
|
|
256
|
-
DROP POLICY IF EXISTS units_delete ON app.units;
|
|
257
|
-
CREATE POLICY units_delete ON app.units FOR DELETE TO authenticated
|
|
258
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('units.manage')));
|
|
259
|
-
|
|
260
|
-
-- roles
|
|
261
|
-
DROP POLICY IF EXISTS roles_select ON app.roles;
|
|
262
|
-
CREATE POLICY roles_select ON app.roles FOR SELECT TO authenticated
|
|
263
|
-
USING ((select app.is_member(tenant_id)));
|
|
264
|
-
DROP POLICY IF EXISTS roles_insert ON app.roles;
|
|
265
|
-
CREATE POLICY roles_insert ON app.roles FOR INSERT TO authenticated
|
|
266
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('roles.manage')) AND NOT is_system);
|
|
267
|
-
DROP POLICY IF EXISTS roles_update ON app.roles;
|
|
268
|
-
CREATE POLICY roles_update ON app.roles FOR UPDATE TO authenticated
|
|
269
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('roles.manage')))
|
|
270
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('roles.manage')));
|
|
271
|
-
DROP POLICY IF EXISTS roles_delete ON app.roles;
|
|
272
|
-
CREATE POLICY roles_delete ON app.roles FOR DELETE TO authenticated
|
|
273
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('roles.manage')) AND NOT is_system);
|
|
274
|
-
|
|
275
|
-
-- role_permissions
|
|
276
|
-
DROP POLICY IF EXISTS role_permissions_select ON app.role_permissions;
|
|
277
|
-
CREATE POLICY role_permissions_select ON app.role_permissions FOR SELECT TO authenticated
|
|
278
|
-
USING ((select app.is_member(tenant_id)));
|
|
279
|
-
DROP POLICY IF EXISTS role_permissions_write ON app.role_permissions;
|
|
280
|
-
CREATE POLICY role_permissions_write ON app.role_permissions FOR ALL TO authenticated
|
|
281
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('roles.manage')))
|
|
282
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('roles.manage')));
|
|
283
|
-
|
|
284
|
-
-- memberships: yourself, or members.read; writes need members.manage and can
|
|
285
|
-
-- never be a self-insert (the P0 of the old model) — creation of the first
|
|
286
|
-
-- member is the create_tenant RPC's job (#134).
|
|
287
|
-
DROP POLICY IF EXISTS memberships_select ON app.memberships;
|
|
288
|
-
CREATE POLICY memberships_select ON app.memberships FOR SELECT TO authenticated
|
|
289
|
-
USING (user_id = (select auth.uid()) OR (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.read'))));
|
|
290
|
-
DROP POLICY IF EXISTS memberships_insert ON app.memberships;
|
|
291
|
-
CREATE POLICY memberships_insert ON app.memberships FOR INSERT TO authenticated
|
|
292
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')) AND user_id <> (select auth.uid()));
|
|
293
|
-
DROP POLICY IF EXISTS memberships_update ON app.memberships;
|
|
294
|
-
CREATE POLICY memberships_update ON app.memberships FOR UPDATE TO authenticated
|
|
295
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')))
|
|
296
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')));
|
|
297
|
-
DROP POLICY IF EXISTS memberships_delete ON app.memberships;
|
|
298
|
-
CREATE POLICY memberships_delete ON app.memberships FOR DELETE TO authenticated
|
|
299
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')) AND user_id <> (select auth.uid()));
|
|
300
|
-
|
|
301
|
-
-- membership_units
|
|
302
|
-
DROP POLICY IF EXISTS membership_units_select ON app.membership_units;
|
|
303
|
-
CREATE POLICY membership_units_select ON app.membership_units FOR SELECT TO authenticated
|
|
304
|
-
USING (user_id = (select auth.uid()) OR (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.read'))));
|
|
305
|
-
DROP POLICY IF EXISTS membership_units_write ON app.membership_units;
|
|
306
|
-
CREATE POLICY membership_units_write ON app.membership_units FOR ALL TO authenticated
|
|
307
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')))
|
|
308
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')));
|
|
309
|
-
|
|
310
|
-
-- user_unit_access: read-only surface (the share table)
|
|
311
|
-
DROP POLICY IF EXISTS user_unit_access_select ON app.user_unit_access;
|
|
312
|
-
CREATE POLICY user_unit_access_select ON app.user_unit_access FOR SELECT TO authenticated
|
|
313
|
-
USING (user_id = (select auth.uid()) OR (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.read'))));
|
|
314
|
-
|
|
315
|
-
-- resource_grants: members read; writing a tuple needs the platform key
|
|
316
|
-
-- authz.grants.manage or the resource's own '{plugin}.manage' (§12.9 default).
|
|
317
|
-
DROP POLICY IF EXISTS resource_grants_select ON app.resource_grants;
|
|
318
|
-
CREATE POLICY resource_grants_select ON app.resource_grants FOR SELECT TO authenticated
|
|
319
|
-
USING ((select app.is_member(tenant_id)));
|
|
320
|
-
DROP POLICY IF EXISTS resource_grants_write ON app.resource_grants;
|
|
321
|
-
CREATE POLICY resource_grants_write ON app.resource_grants FOR ALL TO authenticated
|
|
322
|
-
USING (tenant_id = (select app.current_tenant_id())
|
|
323
|
-
AND ((select app.has_permission('authz.grants.manage')) OR (select app.has_permission(split_part(resource_type, '.', 1) || '.manage'))))
|
|
324
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id())
|
|
325
|
-
AND ((select app.has_permission('authz.grants.manage')) OR (select app.has_permission(split_part(resource_type, '.', 1) || '.manage'))));
|
|
326
|
-
|
|
327
|
-
-- reconciliation log: readable by members.read of the tenant
|
|
328
|
-
DROP POLICY IF EXISTS authz_reconciliation_log_select ON app.authz_reconciliation_log;
|
|
329
|
-
CREATE POLICY authz_reconciliation_log_select ON app.authz_reconciliation_log FOR SELECT TO authenticated
|
|
330
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.read')));
|