16007: Use incremental updates instead of materialized view for permissions
[arvados.git] / services / api / db / migrate / 20200501150153_permission_table.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class PermissionTable < ActiveRecord::Migration[5.0]
6   def up
7     ActiveRecord::Base.connection.execute "DROP MATERIALIZED VIEW IF EXISTS materialized_permission_view;"
8     drop_table :permission_refresh_lock
9
10     create_table :materialized_permissions, :id => false do |t|
11       t.string :user_uuid
12       t.string :target_uuid
13       t.integer :perm_level
14       t.boolean :traverse_owned
15     end
16     add_index :materialized_permissions, [:user_uuid, :target_uuid], unique: true, name: 'permission_user_target'
17     add_index :materialized_permissions, [:target_uuid], unique: false, name: 'permission_target'
18
19     ActiveRecord::Base.connection.execute %{
20 create or replace function project_subtree_with_trash_at (starting_uuid varchar(27), starting_trash_at timestamp)
21 returns table (target_uuid varchar(27), trash_at timestamp)
22 STABLE
23 language SQL
24 as $$
25 WITH RECURSIVE
26         project_subtree(uuid, trash_at) as (
27         values (starting_uuid, starting_trash_at)
28         union
29         select groups.uuid, LEAST(project_subtree.trash_at, groups.trash_at)
30           from groups join project_subtree on (groups.owner_uuid = project_subtree.uuid)
31         )
32         select uuid, trash_at from project_subtree;
33 $$;
34 }
35
36     create_table :trashed_groups, :id => false do |t|
37       t.string :group_uuid
38       t.datetime :trash_at
39     end
40     add_index :trashed_groups, :group_uuid, :unique => true
41
42     ActiveRecord::Base.connection.execute %{
43 create or replace function compute_trashed ()
44 returns table (uuid varchar(27), trash_at timestamp)
45 STABLE
46 language SQL
47 as $$
48 select ps.target_uuid as group_uuid, ps.trash_at from groups,
49   lateral project_subtree_with_trash_at(groups.uuid, groups.trash_at) ps
50   where groups.owner_uuid like '_____-tpzed-_______________'
51 $$;
52 }
53
54     ActiveRecord::Base.connection.execute("INSERT INTO trashed_groups select * from compute_trashed()")
55
56     ActiveRecord::Base.connection.execute %{
57 create or replace function should_traverse_owned (starting_uuid varchar(27),
58                                                   starting_perm integer)
59   returns bool
60 STABLE
61 language SQL
62 as $$
63 select starting_uuid like '_____-j7d0g-_______________' or
64        (starting_uuid like '_____-tpzed-_______________' and starting_perm >= 3);
65 $$;
66 }
67
68     ActiveRecord::Base.connection.execute %{
69 create view permission_graph_edges as
70   select groups.owner_uuid as tail_uuid, groups.uuid as head_uuid, (3) as val from groups
71 union all
72   select users.owner_uuid as tail_uuid, users.uuid as head_uuid, (3) as val from users
73 union all
74   select links.tail_uuid,
75          links.head_uuid,
76          CASE
77            WHEN links.name = 'can_read'   THEN 1
78            WHEN links.name = 'can_login'  THEN 1
79            WHEN links.name = 'can_write'  THEN 2
80            WHEN links.name = 'can_manage' THEN 3
81           END as val
82       from links
83       where links.link_class='permission'
84 }
85
86         # Get a set of permission by searching the graph and following
87         # ownership and permission links.
88         #
89         # edges() - a subselect with the union of ownership and permission links
90         #
91         # traverse_graph() - recursive query, from the starting node,
92         # self-join with edges to find outgoing permissions.
93         # Re-runs the query on new rows until there are no more results.
94         # This accomplishes a breadth-first search of the permission graph.
95         #
96     ActiveRecord::Base.connection.execute %{
97 create or replace function search_permission_graph (starting_uuid varchar(27),
98                                                     starting_perm integer)
99   returns table (target_uuid varchar(27), val integer, traverse_owned bool)
100 STABLE
101 language SQL
102 as $$
103 WITH RECURSIVE
104         traverse_graph(target_uuid, val, traverse_owned) as (
105             values (starting_uuid, starting_perm,
106                     should_traverse_owned(starting_uuid, starting_perm))
107           union
108             (select edges.head_uuid,
109                     least(edges.val, traverse_graph.val,
110                           case traverse_graph.traverse_owned
111                             when true then null
112                             else 0
113                           end),
114                     should_traverse_owned(edges.head_uuid, edges.val)
115              from permission_graph_edges as edges, traverse_graph
116              where traverse_graph.target_uuid = edges.tail_uuid))
117         select target_uuid, max(val), bool_or(traverse_owned) from traverse_graph
118         group by (target_uuid);
119 $$;
120 }
121
122     ActiveRecord::Base.connection.execute %{
123 create or replace function compute_permission_subgraph (perm_origin_uuid varchar(27),
124                                                         starting_uuid varchar(27),
125                                                         starting_perm integer)
126 returns table (user_uuid varchar(27), target_uuid varchar(27), val integer, traverse_owned bool)
127 STABLE
128 language SQL
129 as $$
130 with
131 perm_from_start(perm_origin_uuid, target_uuid, val, traverse_owned) as (
132   select perm_origin_uuid, target_uuid, val, traverse_owned
133     from search_permission_graph(starting_uuid, starting_perm)),
134
135   additional_perms(perm_origin_uuid, target_uuid, val, traverse_owned) as (
136     select edges.tail_uuid as perm_origin_uuid, ps.target_uuid, ps.val,
137            should_traverse_owned(ps.target_uuid, ps.val)
138       from permission_graph_edges as edges, lateral search_permission_graph(edges.head_uuid, edges.val) as ps
139       where (not (edges.tail_uuid = perm_origin_uuid and
140                  edges.head_uuid = starting_uuid)) and
141             edges.tail_uuid not in (select target_uuid from perm_from_start) and
142             edges.head_uuid in (select target_uuid from perm_from_start)),
143
144   partial_perms(perm_origin_uuid, target_uuid, val, traverse_owned) as (
145       select * from perm_from_start
146     union all
147       select * from additional_perms
148   ),
149
150   user_identity_perms(perm_origin_uuid, target_uuid, val, traverse_owned) as (
151     select users.uuid as perm_origin_uuid, ps.target_uuid, ps.val, ps.traverse_owned
152       from users, lateral search_permission_graph(users.uuid, 3) as ps
153       where (users.owner_uuid not in (select target_uuid from partial_perms) or
154              users.owner_uuid = users.uuid) and
155       users.uuid in (select target_uuid from partial_perms)
156   ),
157
158   all_perms(perm_origin_uuid, target_uuid, val, traverse_owned) as (
159       select * from partial_perms
160     union
161       select * from user_identity_perms
162   )
163
164   select v.user_uuid, v.target_uuid, max(v.perm_level), bool_or(v.traverse_owned) from
165     (select m.user_uuid,
166          u.target_uuid,
167          least(u.val, m.perm_level) as perm_level,
168          u.traverse_owned
169       from all_perms as u, materialized_permissions as m
170            where u.perm_origin_uuid = m.target_uuid AND m.traverse_owned
171     union all
172       select perm_origin_uuid as user_uuid, target_uuid, val as perm_level, traverse_owned
173         from all_perms
174         where all_perms.perm_origin_uuid like '_____-tpzed-_______________') as v
175     group by v.user_uuid, v.target_uuid
176 $$;
177      }
178
179     ActiveRecord::Base.connection.execute %{
180 INSERT INTO materialized_permissions
181 select users.uuid, g.target_uuid, g.val, g.traverse_owned
182 from users, lateral search_permission_graph(users.uuid, 3) as g where g.val > 0
183 }
184   end
185
186   def down
187     drop_table :materialized_permissions
188     drop_table :trashed_groups
189
190     ActiveRecord::Base.connection.execute "DROP function project_subtree_with_trash_at (varchar, timestamp);"
191     ActiveRecord::Base.connection.execute "DROP function compute_trashed ();"
192     ActiveRecord::Base.connection.execute "DROP function search_permission_graph(varchar, integer);"
193     ActiveRecord::Base.connection.execute "DROP function compute_permission_subgraph (varchar, varchar, integer);"
194     ActiveRecord::Base.connection.execute "DROP function should_traverse_owned(varchar, integer);"
195     ActiveRecord::Base.connection.execute "DROP view permission_graph_edges;"
196
197     ActiveRecord::Base.connection.execute(%{
198 CREATE MATERIALIZED VIEW materialized_permission_view AS
199  WITH RECURSIVE perm_value(name, val) AS (
200          VALUES ('can_read'::text,(1)::smallint), ('can_login'::text,1), ('can_write'::text,2), ('can_manage'::text,3)
201         ), perm_edges(tail_uuid, head_uuid, val, follow, trashed) AS (
202          SELECT links.tail_uuid,
203             links.head_uuid,
204             pv.val,
205             ((pv.val = 3) OR (groups.uuid IS NOT NULL)) AS follow,
206             (0)::smallint AS trashed,
207             (0)::smallint AS followtrash
208            FROM ((public.links
209              LEFT JOIN perm_value pv ON ((pv.name = (links.name)::text)))
210              LEFT JOIN public.groups ON (((pv.val < 3) AND ((groups.uuid)::text = (links.head_uuid)::text))))
211           WHERE ((links.link_class)::text = 'permission'::text)
212         UNION ALL
213          SELECT groups.owner_uuid,
214             groups.uuid,
215             3,
216             true AS bool,
217                 CASE
218                     WHEN ((groups.trash_at IS NOT NULL) AND (groups.trash_at < clock_timestamp())) THEN 1
219                     ELSE 0
220                 END AS "case",
221             1
222            FROM public.groups
223         ), perm(val, follow, user_uuid, target_uuid, trashed) AS (
224          SELECT (3)::smallint AS val,
225             true AS follow,
226             (users.uuid)::character varying(32) AS user_uuid,
227             (users.uuid)::character varying(32) AS target_uuid,
228             (0)::smallint AS trashed
229            FROM public.users
230         UNION
231          SELECT (LEAST((perm_1.val)::integer, edges.val))::smallint AS val,
232             edges.follow,
233             perm_1.user_uuid,
234             (edges.head_uuid)::character varying(32) AS target_uuid,
235             ((GREATEST((perm_1.trashed)::integer, edges.trashed) * edges.followtrash))::smallint AS trashed
236            FROM (perm perm_1
237              JOIN perm_edges edges ON ((perm_1.follow AND ((edges.tail_uuid)::text = (perm_1.target_uuid)::text))))
238         )
239  SELECT perm.user_uuid,
240     perm.target_uuid,
241     max(perm.val) AS perm_level,
242         CASE perm.follow
243             WHEN true THEN perm.target_uuid
244             ELSE NULL::character varying
245         END AS target_owner_uuid,
246     max(perm.trashed) AS trashed
247    FROM perm
248   GROUP BY perm.user_uuid, perm.target_uuid,
249         CASE perm.follow
250             WHEN true THEN perm.target_uuid
251             ELSE NULL::character varying
252         END
253   WITH NO DATA;
254 }
255     )
256
257     add_index :materialized_permission_view, [:trashed, :target_uuid], name: 'permission_target_trashed'
258     add_index :materialized_permission_view, [:user_uuid, :trashed, :perm_level], name: 'permission_target_user_trashed_level'
259     create_table :permission_refresh_lock
260
261     ActiveRecord::Base.connection.execute 'REFRESH MATERIALIZED VIEW materialized_permission_view;'
262   end
263 end