1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
5 class Link < ArvadosModel
8 include CommonApiTemplate
10 # Posgresql JSONB columns should NOT be declared as serialized, Rails 5
11 # already know how to properly treat them.
12 attribute :properties, :jsonbHash, default: {}
14 validate :name_links_are_obsolete
15 validate :permission_to_attach_to_objects
16 before_update :restrict_alter_permissions
17 before_update :apply_max_overlapping_permissions
18 before_create :apply_max_overlapping_permissions
19 after_update :delete_overlapping_permissions
20 after_update :call_update_permissions, :if => Proc.new { @need_update_permissions }
21 after_create :call_update_permissions, :if => Proc.new { @need_update_permissions }
22 before_destroy :clear_permissions
23 after_destroy :delete_overlapping_permissions
24 after_destroy :check_permissions
25 before_save :check_need_update_permissions
27 api_accessible :user, extend: :common do |t|
43 def apply_max_overlapping_permissions
44 return if self.link_class != 'permission' || !PermLevel[self.name]
46 lock. # select ... for update
47 where(link_class: 'permission',
48 tail_uuid: self.tail_uuid,
49 head_uuid: self.head_uuid,
50 name: PermLevel.keys).
51 where('uuid <> ?', self.uuid).each do |other|
52 if PermLevel[other.name] > PermLevel[self.name]
53 self.name = other.name
58 def delete_overlapping_permissions
59 return if self.link_class != 'permission'
61 if PermLevel[self.name]
63 lock. # select ... for update
64 where(link_class: 'permission',
65 tail_uuid: self.tail_uuid,
66 head_uuid: self.head_uuid,
67 name: PermLevel.keys).
68 where('uuid <> ?', self.uuid)
69 elsif self.name == 'can_login' &&
70 self.properties.respond_to?(:has_key?) &&
71 self.properties.has_key?('username')
73 lock. # select ... for update
74 where(link_class: 'permission',
75 tail_uuid: self.tail_uuid,
76 head_uuid: self.head_uuid,
78 where('properties @> ?', SafeJSON.dump({'username' => self.properties['username']})).
79 where('uuid <> ?', self.uuid)
82 redundant.each do |link|
83 link.clear_permissions
90 if k = ArvadosModel::resource_class_for_uuid(head_uuid)
96 if k = ArvadosModel::resource_class_for_uuid(tail_uuid)
103 def check_readable_uuid attr, attr_value
104 if attr == 'tail_uuid' &&
106 self.link_class == 'permission' &&
107 attr_value[0..4] != Rails.configuration.ClusterID &&
108 ApiClientAuthorization.remote_host(uuid_prefix: attr_value[0..4]) &&
109 ArvadosModel::resource_class_for_uuid(attr_value) == User
110 # Permission link tail is a remote user (the user permissions
111 # are being granted to), so bypass the standard check that a
112 # referenced object uuid is readable by current user.
114 # We could do a call to the remote cluster to check if the user
115 # in tail_uuid exists. This would detect copy-and-paste errors,
116 # but add another way for the request to fail, and I don't think
117 # it would improve security. It doesn't seem to be worth the
118 # complexity tradeoff.
125 def permission_to_attach_to_objects
126 # Anonymous users cannot write links
127 return false if !current_user
129 # All users can write links that don't affect permissions
130 return true if self.link_class != 'permission'
132 if PERM_LEVEL[self.name].nil?
133 errors.add(:name, "is invalid permission, must be one of 'can_read', 'can_write', 'can_manage', 'can_login'")
137 rsc_class = ArvadosModel::resource_class_for_uuid tail_uuid
138 if rsc_class == Group
139 tail_obj = Group.find_by_uuid(tail_uuid)
141 errors.add(:tail_uuid, "does not exist")
144 if tail_obj.group_class != "role"
145 errors.add(:tail_uuid, "must be a user or role, was group with group_class #{tail_obj.group_class}")
148 elsif rsc_class != User
149 errors.add(:tail_uuid, "must be a user or role")
153 # Administrators can grant permissions
154 return true if current_user.is_admin
156 head_obj = ArvadosModel.find_by_uuid(head_uuid)
159 errors.add(:head_uuid, "does not exist")
163 # No permission links can be pointed to past collection versions
164 if head_obj.is_a?(Collection) && head_obj.current_version_uuid != head_uuid
165 errors.add(:head_uuid, "cannot point to a past version of a collection")
169 # All users can grant permissions on objects they own or can manage
170 return true if current_user.can?(manage: head_obj)
176 def restrict_alter_permissions
177 return true if self.link_class != 'permission' && self.link_class_was != 'permission'
179 return true if current_user.andand.uuid == system_user.uuid
181 if link_class_changed? || tail_uuid_changed? || head_uuid_changed?
182 raise "Can only alter permission link level"
193 def check_need_update_permissions
194 @need_update_permissions = self.link_class == 'permission' && (name != name_was || new_record?)
197 def call_update_permissions
198 update_permissions tail_uuid, head_uuid, PERM_LEVEL[name], self.uuid
199 current_user.forget_cached_group_perms
202 def clear_permissions
203 if self.link_class == 'permission'
204 update_permissions tail_uuid, head_uuid, REVOKE_PERM, self.uuid
205 current_user.forget_cached_group_perms
209 def check_permissions
210 if self.link_class == 'permission'
211 check_permissions_against_full_refresh
215 def name_links_are_obsolete
216 if link_class == 'name'
217 errors.add('name', 'Name links are obsolete')
224 # A user is permitted to create, update or modify a permission link
225 # if and only if they have "manage" permission on the object
226 # indicated by the permission link's head_uuid.
228 # All other links are treated as regular ArvadosModel objects.
230 def ensure_owner_uuid_is_permitted
231 if link_class == 'permission'
232 ob = ArvadosModel.find_by_uuid(head_uuid)
233 raise PermissionDeniedError unless current_user.can?(manage: ob)
234 # All permission links should be owned by the system user.
235 self.owner_uuid = system_user_uuid