16683: Permit granting permissions to remote users
[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   after_update :call_update_permissions
18   after_create :call_update_permissions
19   before_destroy :clear_permissions
20   after_destroy :check_permissions
21
22   api_accessible :user, extend: :common do |t|
23     t.add :tail_uuid
24     t.add :link_class
25     t.add :name
26     t.add :head_uuid
27     t.add :head_kind
28     t.add :tail_kind
29     t.add :properties
30   end
31
32   def head_kind
33     if k = ArvadosModel::resource_class_for_uuid(head_uuid)
34       k.kind
35     end
36   end
37
38   def tail_kind
39     if k = ArvadosModel::resource_class_for_uuid(tail_uuid)
40       k.kind
41     end
42   end
43
44   protected
45
46   def check_readable_uuid attr, attr_value
47     if attr == 'tail_uuid' &&
48        !attr_value.nil? &&
49        self.link_class == 'permission' &&
50        attr_value[0..4] != Rails.configuration.ClusterID &&
51        ArvadosModel::resource_class_for_uuid(attr_value) == User
52       # Permission link tail is a remote user (the user permissions
53       # are being granted to), so bypass the standard check that a
54       # referenced object uuid is readable by current user.
55       #
56       # We could do a call to the remote cluster to check if the user
57       # in tail_uuid exists.  This would detect copy-and-paste errors,
58       # but add another way for the request to fail, and I don't think
59       # it would improve security.  It doesn't seem to be worth the
60       # complexity tradeoff.
61       true
62     else
63       super
64     end
65   end
66
67   def permission_to_attach_to_objects
68     # Anonymous users cannot write links
69     return false if !current_user
70
71     # All users can write links that don't affect permissions
72     return true if self.link_class != 'permission'
73
74     if PERM_LEVEL[self.name].nil?
75       errors.add(:name, "is invalid permission, must be one of 'can_read', 'can_write', 'can_manage', 'can_login'")
76       return false
77     end
78
79     rsc_class = ArvadosModel::resource_class_for_uuid tail_uuid
80     if rsc_class == Group
81       tail_obj = Group.find_by_uuid(tail_uuid)
82       if tail_obj.nil?
83         errors.add(:tail_uuid, "does not exist")
84         return false
85       end
86       if tail_obj.group_class != "role"
87         errors.add(:tail_uuid, "must be a user or role, was group with group_class #{tail_obj.group_class}")
88         return false
89       end
90     elsif rsc_class != User
91       errors.add(:tail_uuid, "must be a user or role")
92       return false
93     end
94
95     # Administrators can grant permissions
96     return true if current_user.is_admin
97
98     head_obj = ArvadosModel.find_by_uuid(head_uuid)
99
100     if head_obj.nil?
101       errors.add(:head_uuid, "does not exist")
102       return false
103     end
104
105     # No permission links can be pointed to past collection versions
106     if head_obj.is_a?(Collection) && head_obj.current_version_uuid != head_uuid
107       errors.add(:head_uuid, "cannot point to a past version of a collection")
108       return false
109     end
110
111     # All users can grant permissions on objects they own or can manage
112     return true if current_user.can?(manage: head_obj)
113
114     # Default = deny.
115     false
116   end
117
118   def restrict_alter_permissions
119     return true if self.link_class != 'permission' && self.link_class_was != 'permission'
120
121     return true if current_user.andand.uuid == system_user.uuid
122
123     if link_class_changed? || tail_uuid_changed? || head_uuid_changed?
124       raise "Can only alter permission link level"
125     end
126   end
127
128   PERM_LEVEL = {
129     'can_read' => 1,
130     'can_login' => 1,
131     'can_write' => 2,
132     'can_manage' => 3,
133   }
134
135   def call_update_permissions
136     if self.link_class == 'permission'
137       update_permissions tail_uuid, head_uuid, PERM_LEVEL[name], self.uuid
138     end
139   end
140
141   def clear_permissions
142     if self.link_class == 'permission'
143       update_permissions tail_uuid, head_uuid, REVOKE_PERM, self.uuid
144     end
145   end
146
147   def check_permissions
148     if self.link_class == 'permission'
149       check_permissions_against_full_refresh
150     end
151   end
152
153   def name_links_are_obsolete
154     if link_class == 'name'
155       errors.add('name', 'Name links are obsolete')
156       false
157     else
158       true
159     end
160   end
161
162   # A user is permitted to create, update or modify a permission link
163   # if and only if they have "manage" permission on the object
164   # indicated by the permission link's head_uuid.
165   #
166   # All other links are treated as regular ArvadosModel objects.
167   #
168   def ensure_owner_uuid_is_permitted
169     if link_class == 'permission'
170       ob = ArvadosModel.find_by_uuid(head_uuid)
171       raise PermissionDeniedError unless current_user.can?(manage: ob)
172       # All permission links should be owned by the system user.
173       self.owner_uuid = system_user_uuid
174       return true
175     else
176       super
177     end
178   end
179 end