16007: Almost everything passes
[arvados.git] / services / api / app / models / user.rb
index dd447ca51a895fa2297d6860002a52ff7f360037..f12f72520a3e7ef67139ae4e73e73646f1cd20fc 100644 (file)
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require 'can_be_an_owner'
-require 'refresh_permission_view'
 
 class User < ArvadosModel
   include HasUuid
@@ -32,15 +31,16 @@ class User < ArvadosModel
   before_create :set_initial_username, :if => Proc.new { |user|
     user.username.nil? and user.email
   }
+  after_create :update_permissions
   after_create :setup_on_activate
   after_create :add_system_group_permission_link
-  after_create :invalidate_permissions_cache
   after_create :auto_setup_new_user, :if => Proc.new { |user|
     Rails.configuration.Users.AutoSetupNewUsers and
     (user.uuid != system_user_uuid) and
     (user.uuid != anonymous_user_uuid)
   }
   after_create :send_admin_notifications
+  after_update :update_permissions, :if => :owner_uuid_changed?
   after_update :send_profile_created_notification
   after_update :sync_repository_names, :if => Proc.new { |user|
     (user.uuid != system_user_uuid) and
@@ -48,6 +48,7 @@ class User < ArvadosModel
     (not user.username_was.nil?)
   }
 
+
   has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
   has_many :repositories, foreign_key: :owner_uuid, primary_key: :uuid
 
@@ -143,24 +144,185 @@ class User < ArvadosModel
     true
   end
 
-  def self.invalidate_permissions_cache(async=false)
-    refresh_permission_view(async)
+  def update_permissions
+
+#       puts "Update permissions for #{uuid}"
+#     User.printdump %{
+# select * from materialized_permissions where user_uuid='#{uuid}'
+# }
+#     puts "---"
+    User.update_permissions self.owner_uuid, self.uuid, 3
+
+#   puts "post-update"
+#    User.printdump %{
+# select * from materialized_permissions where user_uuid='#{uuid}'
+# }
+#    puts "<<<"
   end
 
-  def invalidate_permissions_cache
-    User.invalidate_permissions_cache
+  def self.printdump qr
+    q1 = ActiveRecord::Base.connection.exec_query qr
+    q1.each do |r|
+      puts r
+    end
+  end
+
+  def recompute_permissions
+    ActiveRecord::Base.connection.execute("DELETE FROM #{PERMISSION_VIEW} where user_uuid='#{uuid}'")
+    ActiveRecord::Base.connection.execute %{
+INSERT INTO #{PERMISSION_VIEW}
+select '#{uuid}', g.target_uuid, g.val, g.traverse_owned
+from search_permission_graph('#{uuid}', 3) as g
+}
+  end
+
+  def self.update_permissions perm_origin_uuid, starting_uuid, perm_level
+    # Update a subset of the permission graph
+    # perm_level is the inherited permission
+    # perm_level is a number from 0-3
+    #   can_read=1
+    #   can_write=2
+    #   can_manage=3
+    #   call with perm_level=0 to revoke permissions
+    #
+    # 1. Compute set (group, permission) implied by traversing
+    #    graph starting at this group
+    # 2. Find links from outside the graph that point inside
+    # 3. For each starting uuid, get the set of permissions from the
+    #    materialized permission table
+    # 3. Delete permissions from table not in our computed subset.
+    # 4. Upsert each permission in our subset (user, group, val)
+
+    ## testinging
+#     puts "__ update_permissions __"
+#     puts "What's in there now for #{starting_uuid}"
+#     printdump %{
+# select * from materialized_permissions where user_uuid='#{starting_uuid}'
+# }
+
+#     puts "search_permission_graph #{perm_origin_uuid} #{starting_uuid}, #{perm_level}"
+#     printdump %{
+# select '#{perm_origin_uuid}'::varchar as perm_origin_uuid, target_uuid, val, traverse_owned from search_permission_graph('#{starting_uuid}', #{perm_level})
+# }
+
+#     puts "other_links #{perm_origin_uuid} #{starting_uuid}, #{perm_level}"
+#     printdump %{
+# with
+# perm_from_start(perm_origin_uuid, target_uuid, val, traverse_owned) as (
+#   select '#{perm_origin_uuid}'::varchar, target_uuid, val, traverse_owned
+#     from search_permission_graph('#{starting_uuid}'::varchar, #{perm_level}))
+
+#     select links.tail_uuid as perm_origin_uuid, links.head_uuid, links.name
+#       from links
+#       where links.link_class='permission' and
+#         links.tail_uuid not in (select target_uuid from perm_from_start where traverse_owned) and
+#         links.tail_uuid != '#{perm_origin_uuid}' and
+#         links.head_uuid in (select target_uuid from perm_from_start)
+# }
+
+#     puts "additional_perms #{perm_origin_uuid} #{starting_uuid}, #{perm_level}"
+#     printdump %{
+# with
+# perm_from_start(perm_origin_uuid, target_uuid, val, traverse_owned) as (
+#   select '#{perm_origin_uuid}'::varchar, target_uuid, val, traverse_owned
+#     from search_permission_graph('#{starting_uuid}'::varchar, #{perm_level}))
+
+#     select links.tail_uuid as perm_origin_uuid, ps.target_uuid, ps.val, true
+#       from links, lateral search_permission_graph(links.head_uuid,
+#                         CASE
+#                           WHEN links.name = 'can_read'   THEN 1
+#                           WHEN links.name = 'can_login'  THEN 1
+#                           WHEN links.name = 'can_write'  THEN 2
+#                           WHEN links.name = 'can_manage' THEN 3
+#                         END) as ps
+#       where links.link_class='permission' and
+#         links.tail_uuid not in (select target_uuid from perm_from_start where traverse_owned) and
+#         links.tail_uuid != '#{perm_origin_uuid}' and
+#         links.head_uuid in (select target_uuid from perm_from_start)
+# }
+
+#     puts "Perms out"
+#     printdump %{
+# with
+# perm_from_start(perm_origin_uuid, target_uuid, val, traverse_owned) as (
+#   select  '#{perm_origin_uuid}'::varchar, target_uuid, val, traverse_owned
+#     from search_permission_graph('#{starting_uuid}', #{perm_level}))
+
+# (select materialized_permissions.user_uuid, u.target_uuid, max(least(materialized_permissions.perm_level, u.val)), bool_or(u.traverse_owned)
+#   from perm_from_start as u
+#   join materialized_permissions on (u.perm_origin_uuid = materialized_permissions.target_uuid)
+#   where materialized_permissions.traverse_owned
+#   group by materialized_permissions.user_uuid, u.target_uuid)
+# union
+#   select target_uuid as user_uuid, target_uuid, 3, true
+#     from perm_from_start where target_uuid like '_____-tpzed-_______________'
+# }
+    ## end
+
+    temptable_perms = "temp_perms_#{rand(2**64).to_s(10)}"
+    ActiveRecord::Base.connection.exec_query %{
+create temporary table #{temptable_perms} on commit drop
+as select * from compute_permission_subgraph($1, $2, $3)
+},
+                                             'Group.search_permissions',
+                                             [[nil, perm_origin_uuid],
+                                              [nil, starting_uuid],
+                                              [nil, perm_level]]
+
+#     q1 = ActiveRecord::Base.connection.exec_query %{
+# select * from #{temptable_perms} order by user_uuid, target_uuid
+# }
+#     puts "recomputed perms was #{perm_origin_uuid} #{starting_uuid}, #{perm_level}"
+#     q1.each do |r|
+#       puts r
+#     end
+#     puts "<<<<"
+
+    ActiveRecord::Base.connection.exec_query %{
+delete from materialized_permissions where
+  target_uuid in (select target_uuid from #{temptable_perms}) and
+  (user_uuid not in (select user_uuid from #{temptable_perms} where target_uuid=materialized_permissions.target_uuid)
+   or user_uuid in (select user_uuid from #{temptable_perms} where target_uuid=materialized_permissions.target_uuid and perm_level=0))
+}
+
+    ActiveRecord::Base.connection.exec_query %{
+insert into materialized_permissions (user_uuid, target_uuid, perm_level, traverse_owned)
+  select user_uuid, target_uuid, val as perm_level, traverse_owned from #{temptable_perms}
+on conflict (user_uuid, target_uuid) do update set perm_level=EXCLUDED.perm_level, traverse_owned=EXCLUDED.traverse_owned;
+}
+
+    # for testing only - make a copy of the table and compare it to the one generated
+    # using a full permission recompute
+#     temptable_compare = "compare_perms_#{rand(2**64).to_s(10)}"
+#     ActiveRecord::Base.connection.exec_query %{
+# create temporary table #{temptable_compare} on commit drop as select * from materialized_permissions
+# }
+
+    # Ensure a new group can be accessed by the appropriate users
+    # immediately after being created.
+    #User.invalidate_permissions_cache
+
+#     q1 = ActiveRecord::Base.connection.exec_query %{
+# select count(*) from materialized_permissions
+# }
+#     puts "correct version #{q1.first}"
+
+#     q2 = ActiveRecord::Base.connection.exec_query %{
+# select count(*) from #{temptable_compare}
+# }
+#     puts "incremental update #{q2.first}"
   end
 
   # Return a hash of {user_uuid: group_perms}
   def self.all_group_permissions
     all_perms = {}
     ActiveRecord::Base.connection.
-      exec_query("SELECT user_uuid, target_owner_uuid, perm_level, trashed
+      exec_query("SELECT user_uuid, target_uuid, perm_level
                   FROM #{PERMISSION_VIEW}
-                  WHERE target_owner_uuid IS NOT NULL",
+                  WHERE traverse_owned",
                   # "name" arg is a query label that appears in logs:
                   "all_group_permissions",
-                  ).rows.each do |user_uuid, group_uuid, max_p_val, trashed|
+                  ).rows.each do |user_uuid, group_uuid, max_p_val|
       all_perms[user_uuid] ||= {}
       all_perms[user_uuid][group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
     end
@@ -173,15 +335,15 @@ class User < ArvadosModel
   def group_permissions
     group_perms = {self.uuid => {:read => true, :write => true, :manage => true}}
     ActiveRecord::Base.connection.
-      exec_query("SELECT target_owner_uuid, perm_level, trashed
+      exec_query("SELECT target_uuid, perm_level
                   FROM #{PERMISSION_VIEW}
                   WHERE user_uuid = $1
-                  AND target_owner_uuid IS NOT NULL",
+                  AND traverse_owned",
                   # "name" arg is a query label that appears in logs:
-                  "group_permissions for #{uuid}",
+                  "group_permissions_for_user",
                   # "binds" arg is an array of [col_id, value] for '$1' vars:
                   [[nil, uuid]],
-                ).rows.each do |group_uuid, max_p_val, trashed|
+                ).rows.each do |group_uuid, max_p_val|
       group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
     end
     group_perms
@@ -402,7 +564,8 @@ class User < ArvadosModel
       if redirect_to_new_user
         update_attributes!(redirect_to_user_uuid: new_user.uuid, username: nil)
       end
-      invalidate_permissions_cache
+      self.recompute_permissions
+      new_user.recompute_permissions
     end
   end
 
@@ -660,6 +823,7 @@ class User < ArvadosModel
 
   # add the user to the 'All users' group
   def create_user_group_link
+    #puts "In create_user_group_link"
     return (Link.where(tail_uuid: self.uuid,
                        head_uuid: all_users_group[:uuid],
                        link_class: 'permission',