# SPDX-License-Identifier: AGPL-3.0
require 'can_be_an_owner'
-require 'refresh_permission_view'
class User < ArvadosModel
include HasUuid
},
uniqueness: true,
allow_nil: true)
+ validate :must_unsetup_to_deactivate
before_update :prevent_privilege_escalation
before_update :prevent_inactive_admin
before_update :verify_repositories_empty, :if => Proc.new { |user|
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
(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
+
+# 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 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 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
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
end
# create links
- def setup(openid_prefix:, repo_name: nil, vm_uuid: nil)
- oid_login_perm = create_oid_login_perm openid_prefix
+ def setup(repo_name: nil, vm_uuid: nil)
repo_perm = create_user_repo_link repo_name
vm_login_perm = create_vm_login_permission_link(vm_uuid, username) if vm_uuid
group_perm = create_user_group_link
- return [oid_login_perm, repo_perm, vm_login_perm, group_perm, self].compact
+ return [repo_perm, vm_login_perm, group_perm, self].compact
end
# delete user signatures, login, repo, and vm perms, and mark as inactive
def unsetup
# delete oid_login_perms for this user
+ #
+ # note: these permission links are obsolete, they have no effect
+ # on anything and they are not created for new users.
Link.where(tail_uuid: self.email,
link_class: 'permission',
name: 'can_login').destroy_all
self.save!
end
+ def must_unsetup_to_deactivate
+ if self.is_active_changed? &&
+ self.is_active_was == true &&
+ !self.is_active
+
+ group = Group.where(name: 'All users').select do |g|
+ g[:uuid].match(/-f+$/)
+ end.first
+
+ # When a user is set up, they are added to the "All users"
+ # group. A user that is part of the "All users" group is
+ # allowed to self-activate.
+ #
+ # It doesn't make sense to deactivate a user (set is_active =
+ # false) without first removing them from the "All users" group,
+ # because they would be able to immediately reactivate
+ # themselves.
+ #
+ # The 'unsetup' method removes the user from the "All users"
+ # group (and also sets is_active = false) so send a message
+ # explaining the correct way to deactivate a user.
+ #
+ if Link.where(tail_uuid: self.uuid,
+ head_uuid: group[:uuid],
+ link_class: 'permission',
+ name: 'can_read').any?
+ errors.add :is_active, "cannot be set to false directly, use the 'Deactivate' button on Workbench, or the 'unsetup' API call"
+ end
+ end
+ end
+
def set_initial_username(requested: false)
if !requested.is_a?(String) || requested.empty?
email_parts = email.partition("@")
# responsible for checking permission to do this.
def merge(new_owner_uuid:, new_user_uuid:, redirect_to_new_user:)
raise PermissionDeniedError if !current_user.andand.is_admin
- raise "not implemented" if !new_user_uuid
+ raise "Missing new_owner_uuid" if !new_owner_uuid
+ raise "Missing new_user_uuid" if !new_user_uuid
transaction(requires_new: true) do
reload
raise "cannot merge an already merged user" if self.redirect_to_user_uuid
raise "user does not exist" if !new_user
raise "cannot merge to an already merged user" if new_user.redirect_to_user_uuid
- if redirect_to_new_user
+ # 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'
+ # user account that takes over the 'self' account.
+ #
+ # If 'self' is a local user, it is okay to transfer
+ # authorizations, even if the 'new' user is a remote account,
+ # because the remote site does not gain the ability to access an
+ # account it could not before.
+
+ if redirect_to_new_user and self.uuid[0..4] == Rails.configuration.ClusterID
# Existing API tokens and ssh keys are updated to authenticate
# to the new user.
ApiClientAuthorization.
end
# Need to update repository names to new username
- old_repo_name_re = /^#{Regexp.escape(username)}\//
- Repository.where(:owner_uuid => uuid).each do |repo|
- repo.owner_uuid = new_user.uuid
- repo_name_sub = "#{new_user.username}/"
- name = repo.name.sub(old_repo_name_re, repo_name_sub)
- while (conflict = Repository.where(:name => name).first) != nil
- repo_name_sub += "migrated"
+ if username
+ old_repo_name_re = /^#{Regexp.escape(username)}\//
+ Repository.where(:owner_uuid => uuid).each do |repo|
+ repo.owner_uuid = new_user.uuid
+ repo_name_sub = "#{new_user.username}/"
name = repo.name.sub(old_repo_name_re, repo_name_sub)
+ while (conflict = Repository.where(:name => name).first) != nil
+ repo_name_sub += "migrated"
+ name = repo.name.sub(old_repo_name_re, repo_name_sub)
+ end
+ repo.name = name
+ repo.save!
end
- repo.name = name
- repo.save!
end
# References to the merged user's "home project" are updated to
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
user = self
redirects = 0
while (uuid = user.redirect_to_user_uuid)
- user = User.unscoped.find_by_uuid(uuid)
- if !user
- raise Exception.new("user uuid #{user.uuid} redirects to nonexistent uuid #{uuid}")
+ break if uuid.empty?
+ nextuser = User.unscoped.find_by_uuid(uuid)
+ if !nextuser
+ raise Exception.new("user uuid #{user.uuid} redirects to nonexistent uuid '#{uuid}'")
end
+ user = nextuser
redirects += 1
if redirects > 15
raise "Starting from #{self.uuid} redirect_to_user_uuid exceeded maximum number of redirects"
# alternate_emails
# identity_url
- info = info.with_indifferent_access
-
primary_user = nil
# local database
if !primary_user
primary_user = user.redirects_to
elsif primary_user.uuid != user.redirects_to.uuid
- raise "Ambigious email address, directs to both #{primary_user.uuid} and #{user.redirects_to.uuid}"
+ raise "Ambiguous email address, directs to both #{primary_user.uuid} and #{user.redirects_to.uuid}"
end
end
end
:is_admin => false,
:is_active => Rails.configuration.Users.NewUsersAreActive)
- primary_user.set_initial_username(requested: info['username']) if info['username']
+ primary_user.set_initial_username(requested: info['username']) if info['username'] && !info['username'].blank?
primary_user.identity_url = info['identity_url'] if identity_url
end
merged
end
- def create_oid_login_perm(openid_prefix)
- # Check oid_login_perm
- oid_login_perms = Link.where(tail_uuid: self.email,
- head_uuid: self.uuid,
- link_class: 'permission',
- name: 'can_login')
-
- if !oid_login_perms.any?
- # create openid login permission
- oid_login_perm = Link.create!(link_class: 'permission',
- name: 'can_login',
- tail_uuid: self.email,
- head_uuid: self.uuid,
- properties: {
- "identity_url_prefix" => openid_prefix,
- })
- logger.info { "openid login permission: " + oid_login_perm[:uuid] }
- else
- oid_login_perm = oid_login_perms.first
- end
-
- return oid_login_perm
- end
-
def create_user_repo_link(repo_name)
# repo_name is optional
if not repo_name
# 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',
def setup_on_activate
return if [system_user_uuid, anonymous_user_uuid].include?(self.uuid)
if is_active && (new_record? || is_active_changed?)
- setup(openid_prefix: Rails.configuration.default_openid_prefix)
+ setup
end
end
# Automatically setup new user during creation
def auto_setup_new_user
- setup(openid_prefix: Rails.configuration.default_openid_prefix)
+ setup
if username
create_vm_login_permission_link(Rails.configuration.Users.AutoSetupNewUsersWithVmUUID,
username)