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