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
21 after_create :call_update_permissions
22 before_destroy :clear_permissions
23 after_destroy :delete_overlapping_permissions
24 after_destroy :check_permissions
26 api_accessible :user, extend: :common do |t|
42 def apply_max_overlapping_permissions
43 return if self.link_class != 'permission' || !PermLevel[self.name]
45 lock. # select ... for update
46 where(link_class: 'permission',
47 tail_uuid: self.tail_uuid,
48 head_uuid: self.head_uuid,
49 name: PermLevel.keys).
50 where('uuid <> ?', self.uuid).each do |other|
51 if PermLevel[other.name] > PermLevel[self.name]
52 self.name = other.name
57 def delete_overlapping_permissions
58 return if self.link_class != 'permission'
60 if PermLevel[self.name]
62 lock. # select ... for update
63 where(link_class: 'permission',
64 tail_uuid: self.tail_uuid,
65 head_uuid: self.head_uuid,
66 name: PermLevel.keys).
67 where('uuid <> ?', self.uuid)
68 elsif self.name == 'can_login' &&
69 self.properties.respond_to?(:has_key?) &&
70 self.properties.has_key?('username')
72 lock. # select ... for update
73 where(link_class: 'permission',
74 tail_uuid: self.tail_uuid,
75 head_uuid: self.head_uuid,
77 where('properties @> ?', SafeJSON.dump({'username' => self.properties['username']})).
78 where('uuid <> ?', self.uuid)
81 redundant.each do |link|
82 link.clear_permissions
89 if k = ArvadosModel::resource_class_for_uuid(head_uuid)
95 if k = ArvadosModel::resource_class_for_uuid(tail_uuid)
102 def check_readable_uuid attr, attr_value
103 if attr == 'tail_uuid' &&
105 self.link_class == 'permission' &&
106 attr_value[0..4] != Rails.configuration.ClusterID &&
107 ApiClientAuthorization.remote_host(uuid_prefix: attr_value[0..4]) &&
108 ArvadosModel::resource_class_for_uuid(attr_value) == User
109 # Permission link tail is a remote user (the user permissions
110 # are being granted to), so bypass the standard check that a
111 # referenced object uuid is readable by current user.
113 # We could do a call to the remote cluster to check if the user
114 # in tail_uuid exists. This would detect copy-and-paste errors,
115 # but add another way for the request to fail, and I don't think
116 # it would improve security. It doesn't seem to be worth the
117 # complexity tradeoff.
124 def permission_to_attach_to_objects
125 # Anonymous users cannot write links
126 return false if !current_user
128 # All users can write links that don't affect permissions
129 return true if self.link_class != 'permission'
131 if PERM_LEVEL[self.name].nil?
132 errors.add(:name, "is invalid permission, must be one of 'can_read', 'can_write', 'can_manage', 'can_login'")
136 rsc_class = ArvadosModel::resource_class_for_uuid tail_uuid
137 if rsc_class == Group
138 tail_obj = Group.find_by_uuid(tail_uuid)
140 errors.add(:tail_uuid, "does not exist")
143 if tail_obj.group_class != "role"
144 errors.add(:tail_uuid, "must be a user or role, was group with group_class #{tail_obj.group_class}")
147 elsif rsc_class != User
148 errors.add(:tail_uuid, "must be a user or role")
152 # Administrators can grant permissions
153 return true if current_user.is_admin
155 head_obj = ArvadosModel.find_by_uuid(head_uuid)
158 errors.add(:head_uuid, "does not exist")
162 # No permission links can be pointed to past collection versions
163 if head_obj.is_a?(Collection) && head_obj.current_version_uuid != head_uuid
164 errors.add(:head_uuid, "cannot point to a past version of a collection")
168 # All users can grant permissions on objects they own or can manage
169 return true if current_user.can?(manage: head_obj)
175 def restrict_alter_permissions
176 return true if self.link_class != 'permission' && self.link_class_was != 'permission'
178 return true if current_user.andand.uuid == system_user.uuid
180 if link_class_changed? || tail_uuid_changed? || head_uuid_changed?
181 raise "Can only alter permission link level"
192 def call_update_permissions
193 if self.link_class == 'permission'
194 update_permissions tail_uuid, head_uuid, PERM_LEVEL[name], self.uuid
195 current_user.forget_cached_group_perms
199 def clear_permissions
200 if self.link_class == 'permission'
201 update_permissions tail_uuid, head_uuid, REVOKE_PERM, self.uuid
202 current_user.forget_cached_group_perms
206 def check_permissions
207 if self.link_class == 'permission'
208 check_permissions_against_full_refresh
212 def name_links_are_obsolete
213 if link_class == 'name'
214 errors.add('name', 'Name links are obsolete')
221 # A user is permitted to create, update or modify a permission link
222 # if and only if they have "manage" permission on the object
223 # indicated by the permission link's head_uuid.
225 # All other links are treated as regular ArvadosModel objects.
227 def ensure_owner_uuid_is_permitted
228 if link_class == 'permission'
229 ob = ArvadosModel.find_by_uuid(head_uuid)
230 raise PermissionDeniedError unless current_user.can?(manage: ob)
231 # All permission links should be owned by the system user.
232 self.owner_uuid = system_user_uuid