16007: Use incremental updates instead of materialized view for permissions
[arvados.git] / services / api / lib / refresh_permission_view.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 PERMISSION_VIEW = "materialized_permissions"
6 TRASHED_GROUPS = "trashed_groups"
7
8 def refresh_permission_view
9   ActiveRecord::Base.transaction do
10     ActiveRecord::Base.connection.execute("LOCK TABLE #{PERMISSION_VIEW}")
11     ActiveRecord::Base.connection.execute("DELETE FROM #{PERMISSION_VIEW}")
12     ActiveRecord::Base.connection.execute %{
13 INSERT INTO #{PERMISSION_VIEW}
14 select users.uuid, g.target_uuid, g.val, g.traverse_owned
15 from users, lateral search_permission_graph(users.uuid, 3) as g where g.val > 0
16 },
17                                           "refresh_permission_view.do"
18   end
19 end
20
21 def refresh_trashed
22   ActiveRecord::Base.connection.execute("DELETE FROM #{TRASHED_GROUPS}")
23   ActiveRecord::Base.connection.execute("INSERT INTO #{TRASHED_GROUPS} select * from compute_trashed()")
24 end
25
26 def update_permissions perm_origin_uuid, starting_uuid, perm_level, check=false
27   # Update a subset of the permission graph
28   # perm_level is the inherited permission
29   # perm_level is a number from 0-3
30   #   can_read=1
31   #   can_write=2
32   #   can_manage=3
33   #   call with perm_level=0 to revoke permissions
34   #
35   # 1. Compute set (group, permission) implied by traversing
36   #    graph starting at this group
37   # 2. Find links from outside the graph that point inside
38   # 3. For each starting uuid, get the set of permissions from the
39   #    materialized permission table
40   # 3. Delete permissions from table not in our computed subset.
41   # 4. Upsert each permission in our subset (user, group, val)
42
43   ActiveRecord::Base.connection.execute "LOCK TABLE #{PERMISSION_VIEW} in SHARE MODE"
44
45   ActiveRecord::Base.connection.exec_query "SET LOCAL enable_mergejoin to false;"
46
47   temptable_perms = "temp_perms_#{rand(2**64).to_s(10)}"
48   ActiveRecord::Base.connection.exec_query %{
49 create temporary table #{temptable_perms} on commit drop
50 as select * from compute_permission_subgraph($1, $2, $3)
51 },
52                                            'update_permissions.select',
53                                            [[nil, perm_origin_uuid],
54                                             [nil, starting_uuid],
55                                             [nil, perm_level]]
56
57   ActiveRecord::Base.connection.exec_query "SET LOCAL enable_mergejoin to true;"
58
59   ActiveRecord::Base.connection.exec_delete %{
60 delete from #{PERMISSION_VIEW} where
61   target_uuid in (select target_uuid from #{temptable_perms}) and
62   not exists (select 1 from #{temptable_perms}
63               where target_uuid=#{PERMISSION_VIEW}.target_uuid and
64                     user_uuid=#{PERMISSION_VIEW}.user_uuid and
65                     val>0)
66 },
67                                         "update_permissions.delete"
68
69   ActiveRecord::Base.connection.exec_query %{
70 insert into #{PERMISSION_VIEW} (user_uuid, target_uuid, perm_level, traverse_owned)
71   select user_uuid, target_uuid, val as perm_level, traverse_owned from #{temptable_perms} where val>0
72 on conflict (user_uuid, target_uuid) do update set perm_level=EXCLUDED.perm_level, traverse_owned=EXCLUDED.traverse_owned;
73 },
74                                            "update_permissions.insert"
75
76   if check and perm_level>0
77     check_permissions_against_full_refresh
78   end
79 end
80
81
82 def check_permissions_against_full_refresh
83   #
84   # For debugging, this checks contents of the
85   # incrementally-updated 'materialized_permission' against a
86   # from-scratch permission refresh.
87   #
88
89   q1 = ActiveRecord::Base.connection.exec_query %{
90 select user_uuid, target_uuid, perm_level, traverse_owned from #{PERMISSION_VIEW}
91 order by user_uuid, target_uuid
92 }, "check_permissions_against_full_refresh.permission_table"
93
94   q2 = ActiveRecord::Base.connection.exec_query %{
95 select users.uuid as user_uuid, g.target_uuid, g.val as perm_level, g.traverse_owned
96 from users, lateral search_permission_graph(users.uuid, 3) as g where g.val > 0
97 order by users.uuid, target_uuid
98 }, "check_permissions_against_full_refresh.full_recompute"
99
100   if q1.count != q2.count
101     puts "Didn't match incremental+: #{q1.count} != full refresh-: #{q2.count}"
102   end
103
104   if q1.count > q2.count
105     q1.each_with_index do |r, i|
106       if r != q2[i]
107         puts "+#{r}\n-#{q2[i]}"
108         raise "Didn't match"
109       end
110     end
111   else
112     q2.each_with_index do |r, i|
113       if r != q1[i]
114         puts "+#{q1[i]}\n-#{r}"
115         raise "Didn't match"
116       end
117     end
118   end
119 end