20259: Add documentation for banner and tooltip features
[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
21   after_create :call_update_permissions
22   before_destroy :clear_permissions
23   after_destroy :delete_overlapping_permissions
24   after_destroy :check_permissions
25
26   api_accessible :user, extend: :common do |t|
27     t.add :tail_uuid
28     t.add :link_class
29     t.add :name
30     t.add :head_uuid
31     t.add :head_kind
32     t.add :tail_kind
33     t.add :properties
34   end
35
36   PermLevel = {
37     'can_read' => 0,
38     'can_write' => 1,
39     'can_manage' => 2,
40   }
41
42   def apply_max_overlapping_permissions
43     return if self.link_class != 'permission' || !PermLevel[self.name]
44     Link.
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
53       end
54     end
55   end
56
57   def delete_overlapping_permissions
58     return if self.link_class != 'permission'
59     redundant = nil
60     if PermLevel[self.name]
61       redundant = Link.
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')
71       redundant = Link.
72                     lock. # select ... for update
73                     where(link_class: 'permission',
74                           tail_uuid: self.tail_uuid,
75                           head_uuid: self.head_uuid,
76                           name: 'can_login').
77                     where('properties @> ?', SafeJSON.dump({'username' => self.properties['username']})).
78                     where('uuid <> ?', self.uuid)
79     end
80     if redundant
81       redundant.each do |link|
82         link.clear_permissions
83       end
84       redundant.delete_all
85     end
86   end
87
88   def head_kind
89     if k = ArvadosModel::resource_class_for_uuid(head_uuid)
90       k.kind
91     end
92   end
93
94   def tail_kind
95     if k = ArvadosModel::resource_class_for_uuid(tail_uuid)
96       k.kind
97     end
98   end
99
100   protected
101
102   def check_readable_uuid attr, attr_value
103     if attr == 'tail_uuid' &&
104        !attr_value.nil? &&
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.
112       #
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.
118       true
119     else
120       super
121     end
122   end
123
124   def permission_to_attach_to_objects
125     # Anonymous users cannot write links
126     return false if !current_user
127
128     # All users can write links that don't affect permissions
129     return true if self.link_class != 'permission'
130
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'")
133       return false
134     end
135
136     rsc_class = ArvadosModel::resource_class_for_uuid tail_uuid
137     if rsc_class == Group
138       tail_obj = Group.find_by_uuid(tail_uuid)
139       if tail_obj.nil?
140         errors.add(:tail_uuid, "does not exist")
141         return false
142       end
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}")
145         return false
146       end
147     elsif rsc_class != User
148       errors.add(:tail_uuid, "must be a user or role")
149       return false
150     end
151
152     # Administrators can grant permissions
153     return true if current_user.is_admin
154
155     head_obj = ArvadosModel.find_by_uuid(head_uuid)
156
157     if head_obj.nil?
158       errors.add(:head_uuid, "does not exist")
159       return false
160     end
161
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")
165       return false
166     end
167
168     # All users can grant permissions on objects they own or can manage
169     return true if current_user.can?(manage: head_obj)
170
171     # Default = deny.
172     false
173   end
174
175   def restrict_alter_permissions
176     return true if self.link_class != 'permission' && self.link_class_was != 'permission'
177
178     return true if current_user.andand.uuid == system_user.uuid
179
180     if link_class_changed? || tail_uuid_changed? || head_uuid_changed?
181       raise "Can only alter permission link level"
182     end
183   end
184
185   PERM_LEVEL = {
186     'can_read' => 1,
187     'can_login' => 1,
188     'can_write' => 2,
189     'can_manage' => 3,
190   }
191
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
196     end
197   end
198
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
203     end
204   end
205
206   def check_permissions
207     if self.link_class == 'permission'
208       check_permissions_against_full_refresh
209     end
210   end
211
212   def name_links_are_obsolete
213     if link_class == 'name'
214       errors.add('name', 'Name links are obsolete')
215       false
216     else
217       true
218     end
219   end
220
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.
224   #
225   # All other links are treated as regular ArvadosModel objects.
226   #
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
233       return true
234     else
235       super
236     end
237   end
238 end