21700: Install Bundler system-wide in Rails postinst
[arvados.git] / services / api / app / models / link.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class Link < ArvadosModel
6   include HasUuid
7   include KindAndEtag
8   include CommonApiTemplate
9
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: {}
13
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
26
27   api_accessible :user, extend: :common do |t|
28     t.add :tail_uuid
29     t.add :link_class
30     t.add :name
31     t.add :head_uuid
32     t.add :head_kind
33     t.add :tail_kind
34     t.add :properties
35   end
36
37   PermLevel = {
38     'can_read' => 0,
39     'can_write' => 1,
40     'can_manage' => 2,
41   }
42
43   def apply_max_overlapping_permissions
44     return if self.link_class != 'permission' || !PermLevel[self.name]
45     Link.
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
54       end
55     end
56   end
57
58   def delete_overlapping_permissions
59     return if self.link_class != 'permission'
60     redundant = nil
61     if PermLevel[self.name]
62       redundant = Link.
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')
72       redundant = Link.
73                     lock. # select ... for update
74                     where(link_class: 'permission',
75                           tail_uuid: self.tail_uuid,
76                           head_uuid: self.head_uuid,
77                           name: 'can_login').
78                     where('properties @> ?', SafeJSON.dump({'username' => self.properties['username']})).
79                     where('uuid <> ?', self.uuid)
80     end
81     if redundant
82       redundant.each do |link|
83         link.clear_permissions
84       end
85       redundant.delete_all
86     end
87   end
88
89   def head_kind
90     if k = ArvadosModel::resource_class_for_uuid(head_uuid)
91       k.kind
92     end
93   end
94
95   def tail_kind
96     if k = ArvadosModel::resource_class_for_uuid(tail_uuid)
97       k.kind
98     end
99   end
100
101   protected
102
103   def check_readable_uuid attr, attr_value
104     if attr == 'tail_uuid' &&
105        !attr_value.nil? &&
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.
113       #
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.
119       true
120     else
121       super
122     end
123   end
124
125   def permission_to_attach_to_objects
126     # Anonymous users cannot write links
127     return false if !current_user
128
129     # All users can write links that don't affect permissions
130     return true if self.link_class != 'permission'
131
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'")
134       return false
135     end
136
137     rsc_class = ArvadosModel::resource_class_for_uuid tail_uuid
138     if rsc_class == Group
139       tail_obj = Group.find_by_uuid(tail_uuid)
140       if tail_obj.nil?
141         errors.add(:tail_uuid, "does not exist")
142         return false
143       end
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}")
146         return false
147       end
148     elsif rsc_class != User
149       errors.add(:tail_uuid, "must be a user or role")
150       return false
151     end
152
153     # Administrators can grant permissions
154     return true if current_user.is_admin
155
156     head_obj = ArvadosModel.find_by_uuid(head_uuid)
157
158     if head_obj.nil?
159       errors.add(:head_uuid, "does not exist")
160       return false
161     end
162
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")
166       return false
167     end
168
169     # All users can grant permissions on objects they own or can manage
170     return true if current_user.can?(manage: head_obj)
171
172     # Default = deny.
173     false
174   end
175
176   def restrict_alter_permissions
177     return true if self.link_class != 'permission' && self.link_class_was != 'permission'
178
179     return true if current_user.andand.uuid == system_user.uuid
180
181     if link_class_changed? || tail_uuid_changed? || head_uuid_changed?
182       raise "Can only alter permission link level"
183     end
184   end
185
186   PERM_LEVEL = {
187     'can_read' => 1,
188     'can_login' => 1,
189     'can_write' => 2,
190     'can_manage' => 3,
191   }
192
193   def check_need_update_permissions
194     @need_update_permissions = self.link_class == 'permission' && (name != name_was || new_record?)
195   end
196
197   def call_update_permissions
198       update_permissions tail_uuid, head_uuid, PERM_LEVEL[name], self.uuid
199       current_user.forget_cached_group_perms
200   end
201
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
206     end
207   end
208
209   def check_permissions
210     if self.link_class == 'permission'
211       check_permissions_against_full_refresh
212     end
213   end
214
215   def name_links_are_obsolete
216     if link_class == 'name'
217       errors.add('name', 'Name links are obsolete')
218       false
219     else
220       true
221     end
222   end
223
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.
227   #
228   # All other links are treated as regular ArvadosModel objects.
229   #
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
236       return true
237     else
238       super
239     end
240   end
241 end