attribute :properties, :jsonbHash, default: {}
validate :ensure_filesystem_compatible_name
- after_create :invalidate_permissions_cache
- after_update :maybe_invalidate_permissions_cache
before_create :assign_name
+ after_create :update_permissions
+ after_create :update_trash
+
+ after_update :update_permissions
+ after_update :update_trash
+
+ after_destroy :clear_permissions_and_trash
+
api_accessible :user, extend: :common do |t|
t.add :name
super if group_class == 'project'
end
- def maybe_invalidate_permissions_cache
- if trash_at_changed? or owner_uuid_changed?
+ def update_trash
+ if trash_at_changed? or owner_uuid_changed? or (new_record? and !trash_at.nil?)
# The group was added or removed from the trash.
#
# Strategy:
on conflict (group_uuid) do update set trash_at=EXCLUDED.trash_at;
}
end
-
- if uuid_changed? or owner_uuid_changed?
- # This can change users' permissions on other groups as well as
- # this one.
- invalidate_permissions_cache
- end
end
- def invalidate_permissions_cache
- # Ensure a new group can be accessed by the appropriate users
- # immediately after being created.
- User.invalidate_permissions_cache self.async_permissions_update
-
+ def update_permissions
if new_record? or owner_uuid_changed?
- # 1. Compute set (group, permission) implied by traversing
- # graph starting at this group
- # 2. Find links from outside the graph that point inside
- # 3. We now have the full set of permissions for a subset of groups.
- # 3. Upsert each permission in our subset (user, group, val)
- # 4. Delete permissions from table not in our computed subset.
-
- temptable_perm_from_start = "perm_from_start_#{rand(2**64).to_s(10)}"
- ActiveRecord::Base.connection.exec_query %{
-create temporary table #{temptable_perm_from_start} on commit drop
-as select $1::varchar as starting_uuid, target_uuid, val
-from search_permission_graph($1::varchar, 3::smallint)
-},
- 'Group.search_permissions',
- [[nil, self.uuid]]
+ User.update_permissions self.owner_uuid, self.uuid, 3
+ end
+ end
- temptable_additional_perms = "additional_perms_#{rand(2**64).to_s(10)}"
- ActiveRecord::Base.connection.exec_query %{
-create temporary table #{temptable_additional_perms} on commit drop
-as select links.tail_uuid as starting_uuid, ps.target_uuid, ps.val
- from links, lateral search_permission_graph(links.head_uuid::varchar, CASE
-WHEN links.name = 'can_read' THEN 1::smallint
-WHEN links.name = 'can_login' THEN 1::smallint
-WHEN links.name = 'can_write' THEN 2::smallint
-WHEN links.name = 'can_manage' THEN 3::smallint
-END) as ps
-where links.link_class='permission' and
- links.tail_uuid not in (select target_uuid from #{temptable_perm_from_start}) and
- links.head_uuid in (select target_uuid from #{temptable_perm_from_start})
-}
+ def clear_permissions_and_trash
+ User.update_permissions self.owner_uuid, self.uuid, 0
+ ActiveRecord::Base.connection.exec_query %{
+delete from trashed_groups where group_uuid=$1
+}, "Group.clear_trash", [[nil, self.uuid]]
- q1 = ActiveRecord::Base.connection.exec_query "select * from #{temptable_perm_from_start}"
- q1.each do |r|
- #puts r
- end
- end
end
def assign_name
# already know how to properly treat them.
attribute :properties, :jsonbHash, default: {}
+ validate :name_links_are_obsolete
before_create :permission_to_attach_to_objects
before_update :permission_to_attach_to_objects
- after_update :maybe_invalidate_permissions_cache
- after_create :maybe_invalidate_permissions_cache
- after_destroy :maybe_invalidate_permissions_cache
- validate :name_links_are_obsolete
+ after_update :update_permissions
+ after_create :update_permissions
+ after_destroy :clear_permissions
api_accessible :user, extend: :common do |t|
t.add :tail_uuid
false
end
- def maybe_invalidate_permissions_cache
+ PERM_LEVEL = {
+ 'can_read' => 1,
+ 'can_login' => 1,
+ 'can_write' => 2,
+ 'can_manage' => 3,
+ }
+
+ def update_permissions
+ if self.link_class == 'permission'
+ User.update_permissions tail_uuid, head_uuid, PERM_LEVEL[name]
+ end
+ end
+
+ def clear_permissions
if self.link_class == 'permission'
- # Clearing the entire permissions cache can generate many
- # unnecessary queries if many active users are not affected by
- # this change. In such cases it would be better to search cached
- # permissions for head_uuid and tail_uuid, and invalidate the
- # cache for only those users. (This would require a browseable
- # cache.)
- User.invalidate_permissions_cache
+ User.update_permissions tail_uuid, head_uuid, 0
end
end
# SPDX-License-Identifier: AGPL-3.0
require 'can_be_an_owner'
-require 'refresh_permission_view'
class User < ArvadosModel
include HasUuid
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
after_update :send_profile_created_notification
after_update :sync_repository_names, :if => Proc.new { |user|
(user.uuid != system_user_uuid) and
(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
true
end
- def self.invalidate_permissions_cache(async=false)
- refresh_permission_view(async)
+ def update_permissions
+ if owner_uuid_changed?
+ puts "Update permissions for #{uuid} #{new_record?}"
+ User.printdump %{
+select * from materialized_permissions where user_uuid='#{uuid}'
+}
+ puts "---"
+ User.update_permissions self.owner_uuid, self.uuid, 3
+ User.printdump %{
+select * from materialized_permissions where user_uuid='#{uuid}'
+}
+
+ end
+ end
+
+ def self.printdump qr
+ q1 = ActiveRecord::Base.connection.exec_query qr
+ q1.each do |r|
+ puts r
+ end
end
- def invalidate_permissions_cache
- User.invalidate_permissions_cache
+ 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 "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 "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}
+}
+ puts "recomputed perms was #{perm_origin_uuid} #{starting_uuid}, #{perm_level}"
+ q1.each do |r|
+ puts r
+ end
+
+ 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}
raise "user does not exist" if !new_user
raise "cannot merge to an already merged user" if new_user.redirect_to_user_uuid
+ User.update_permissions self.owner_uuid, self.uuid, 0
+
# If 'self' is a remote user, don't transfer authorizations
# (i.e. ability to access the account) to the new user, because
# that gives the remote site the ability to access the 'new'
if redirect_to_new_user
update_attributes!(redirect_to_user_uuid: new_user.uuid, username: nil)
end
- invalidate_permissions_cache
+ User.update_permissions self.owner_uuid, self.uuid, 3
+ User.update_permissions new_user.owner_uuid, new_user.uuid, 3
end
end
# 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',
t.integer :perm_level
t.boolean :traverse_owned
end
+ add_index :materialized_permissions, [:user_uuid, :target_uuid], unique: true, name: 'permission_user_target'
ActiveRecord::Base.connection.execute %{
create or replace function project_subtree (starting_uuid varchar(27))
# This accomplishes a breadth-first search of the permission graph.
#
ActiveRecord::Base.connection.execute %{
-create or replace function search_permission_graph (starting_uuid varchar(27), starting_perm integer)
-returns table (target_uuid varchar(27), val integer, traverse_owned bool)
+create or replace function search_permission_graph (starting_uuid varchar(27),
+ starting_perm integer)
+ returns table (target_uuid varchar(27), val integer, traverse_owned bool)
STABLE
language SQL
as $$
$$;
}
+ ActiveRecord::Base.connection.execute %{
+create or replace function compute_permission_subgraph (perm_origin_uuid varchar(27),
+ starting_uuid varchar(27),
+ starting_perm integer)
+returns table (user_uuid varchar(27), target_uuid varchar(27), val integer, traverse_owned bool)
+STABLE
+language SQL
+as $$
+with
+perm_from_start(perm_origin_uuid, target_uuid, val, traverse_owned) as (
+ select perm_origin_uuid, target_uuid, val, traverse_owned
+ from search_permission_graph(starting_uuid, starting_perm)),
+
+ additional_perms(perm_origin_uuid, target_uuid, val, traverse_owned) as (
+ 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) and
+ links.head_uuid in (select target_uuid from perm_from_start))
+
+select materialized_permissions.user_uuid, u.target_uuid, max(least(u.val, materialized_permissions.perm_level)), bool_or(u.traverse_owned)
+ from ((select * from perm_from_start) union (select * from additional_perms)) 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-_______________'
+$$;
+ }
+
ActiveRecord::Base.connection.execute "DROP MATERIALIZED VIEW IF EXISTS materialized_permission_view;"
end
ActiveRecord::Base.connection.execute "DROP function project_subtree_with_trash_at (varchar, timestamp);"
ActiveRecord::Base.connection.execute "DROP function compute_trashed ();"
ActiveRecord::Base.connection.execute "DROP function search_permission_graph(varchar, integer);"
+ ActiveRecord::Base.connection.execute "DROP function compute_permission_subgraph (varchar, varchar, integer);"
end
end
end
end
end
- User.invalidate_permissions_cache
+ #User.invalidate_permissions_cache
+ do_refresh_permission_view
end
end)
end
#
# SPDX-License-Identifier: AGPL-3.0
+require 'refresh_permission_view'
+
ENV["RAILS_ENV"] = "test"
unless ENV["NO_COVERAGE_TEST"]
begin
include ArvadosTestSupport
include CurrentApiClient
- setup do
- do_refresh_permission_view
- ActiveRecord::Base.connection.execute("DELETE FROM #{TRASHED_GROUPS}")
- ActiveRecord::Base.connection.execute("INSERT INTO #{TRASHED_GROUPS} select * from compute_trashed()")
- end
-
teardown do
Thread.current[:api_client_ip_address] = nil
Thread.current[:api_client_authorization] = nil
end
# Ensure permissions are computed from the test fixtures.
-User.invalidate_permissions_cache
+do_refresh_permission_view
+ActiveRecord::Base.connection.execute("DELETE FROM #{TRASHED_GROUPS}")
+ActiveRecord::Base.connection.execute("INSERT INTO #{TRASHED_GROUPS} select * from compute_trashed()")