Merge branch '19929-fill-discovery-document'
[arvados.git] / services / api / app / controllers / arvados / v1 / users_controller.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class Arvados::V1::UsersController < ApplicationController
6   accept_attribute_as_json :prefs, Hash
7   accept_param_as_json :updates
8
9   skip_before_action :find_object_by_uuid, only:
10     [:activate, :current, :system, :setup, :merge, :batch_update]
11   skip_before_action :render_404_if_no_object, only:
12     [:activate, :current, :system, :setup, :merge, :batch_update]
13   before_action :admin_required, only: [:setup, :unsetup, :batch_update]
14
15   # Internal API used by controller to update local cache of user
16   # records from LoginCluster.
17   def batch_update
18     @objects = []
19     # update_remote_user takes a row lock on the User record, so sort
20     # the keys so we always lock them in the same order.
21     sorted = params[:updates].keys.sort
22     sorted.each do |uuid|
23       attrs = params[:updates][uuid]
24       attrs[:uuid] = uuid
25       u = User.update_remote_user nullify_attrs(attrs)
26       @objects << u
27     end
28     @offset = 0
29     @limit = -1
30     render_list
31   end
32
33   def self._current_method_description
34     "Return the user record associated with the API token authorizing this request."
35   end
36
37   def current
38     if current_user
39       @object = current_user
40       show
41     else
42       send_error("Not logged in", status: 401)
43     end
44   end
45
46   def self._system_method_description
47     "Return this cluster's system (\"root\") user record."
48   end
49
50   def system
51     @object = system_user
52     show
53   end
54
55   def self._activate_method_description
56     "Set the `is_active` flag on a user record."
57   end
58
59   def activate
60     if params[:id] and params[:id].match(/\D/)
61       params[:uuid] = params.delete :id
62     end
63     if current_user.andand.is_admin && params[:uuid]
64       @object = User.find_by_uuid params[:uuid]
65     else
66       @object = current_user
67     end
68     if not @object.is_active
69       if @object.uuid[0..4] == Rails.configuration.Login.LoginCluster &&
70          @object.uuid[0..4] != Rails.configuration.ClusterID
71         logger.warn "Local user #{@object.uuid} called users#activate but only LoginCluster can do that"
72         raise ArgumentError.new "cannot activate user #{@object.uuid} here, only the #{@object.uuid[0..4]} cluster can do that"
73       elsif not (current_user.is_admin or @object.is_invited)
74         logger.warn "User #{@object.uuid} called users.activate " +
75           "but is not invited"
76         raise ArgumentError.new "Cannot activate without being invited."
77       end
78       act_as_system_user do
79         required_uuids = Link.where("owner_uuid = ? and link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
80                                     system_user_uuid,
81                                     'signature',
82                                     'require',
83                                     system_user_uuid,
84                                     Collection.uuid_like_pattern).
85           collect(&:head_uuid)
86         signed_uuids = Link.where(owner_uuid: system_user_uuid,
87                                   link_class: 'signature',
88                                   name: 'click',
89                                   tail_uuid: @object.uuid,
90                                   head_uuid: required_uuids).
91           collect(&:head_uuid)
92         todo_uuids = required_uuids - signed_uuids
93         if todo_uuids.empty?
94           @object.update is_active: true
95           logger.info "User #{@object.uuid} activated"
96         else
97           logger.warn "User #{@object.uuid} called users.activate " +
98             "before signing agreements #{todo_uuids.inspect}"
99           raise ArvadosModel::PermissionDeniedError.new \
100           "Cannot activate without user agreements #{todo_uuids.inspect}."
101         end
102       end
103     end
104     show
105   end
106
107   def self._setup_method_description
108     "Convenience method to \"fully\" set up a user record with a virtual machine login and notification email."
109   end
110
111   # create user object and all the needed links
112   def setup
113     if params[:uuid]
114       @object = User.find_by_uuid(params[:uuid])
115       if !@object
116         return render_404_if_no_object
117       end
118     elsif !params[:user] || params[:user].empty?
119       raise ArgumentError.new "Required uuid or user"
120     elsif !params[:user]['email']
121       raise ArgumentError.new "Require user email"
122     else
123       @object = model_class.create! resource_attrs
124     end
125
126     @response = @object.setup(vm_uuid: params[:vm_uuid],
127                               send_notification_email: params[:send_notification_email])
128
129     send_json kind: "arvados#HashList", items: @response.as_api_response(nil)
130   end
131
132   def self._unsetup_method_description
133     "Unset a user's active flag and delete associated records."
134   end
135
136   # delete user agreements, vm, repository, login links; set state to inactive
137   def unsetup
138     reload_object_before_update
139     @object.unsetup
140     show
141   end
142
143   def self._merge_method_description
144     "Transfer ownership of one user's data to another."
145   end
146
147   def merge
148     if (params[:old_user_uuid] || params[:new_user_uuid])
149       if !current_user.andand.is_admin
150         return send_error("Must be admin to use old_user_uuid/new_user_uuid", status: 403)
151       end
152       if !params[:old_user_uuid] || !params[:new_user_uuid]
153         return send_error("Must supply both old_user_uuid and new_user_uuid", status: 422)
154       end
155       new_user = User.find_by_uuid(params[:new_user_uuid])
156       if !new_user
157         return send_error("User in new_user_uuid not found", status: 422)
158       end
159       @object = User.find_by_uuid(params[:old_user_uuid])
160       if !@object
161         return send_error("User in old_user_uuid not found", status: 422)
162       end
163     else
164       if Thread.current[:api_client_authorization].scopes != ['all']
165         return send_error("cannot merge with a scoped token", status: 403)
166       end
167
168       new_auth = ApiClientAuthorization.validate(token: params[:new_user_token])
169       if !new_auth
170         return send_error("invalid new_user_token", status: 401)
171       end
172
173       if new_auth.user.uuid[0..4] == Rails.configuration.ClusterID
174         if new_auth.scopes != ['all']
175           return send_error("supplied new_user_token has restricted scope", status: 403)
176         end
177       end
178       new_user = new_auth.user
179       @object = current_user
180     end
181
182     if @object.uuid == new_user.uuid
183       return send_error("cannot merge user to self", status: 422)
184     end
185
186     if !params[:new_owner_uuid]
187       return send_error("missing new_owner_uuid", status: 422)
188     end
189
190     if !new_user.can?(write: params[:new_owner_uuid])
191       return send_error("cannot move objects into supplied new_owner_uuid: new user does not have write permission", status: 403)
192     end
193
194     act_as_system_user do
195       @object.merge(new_owner_uuid: params[:new_owner_uuid],
196                     new_user_uuid: new_user.uuid,
197                     redirect_to_new_user: params[:redirect_to_new_user])
198     end
199     show
200   end
201
202   protected
203
204   def self._merge_requires_parameters
205     {
206       new_owner_uuid: {
207         type: 'string',
208         required: true,
209         description: "UUID of the user or group that will take ownership of data owned by the old user.",
210       },
211       new_user_token: {
212         type: 'string',
213         required: false,
214         description: "Valid API token for the user receiving ownership. If you use this option, it takes ownership of data owned by the user making the request.",
215       },
216       redirect_to_new_user: {
217         type: 'boolean',
218         required: false,
219         default: false,
220         description: "If true, authorization attempts for the old user will be redirected to the new user.",
221       },
222       old_user_uuid: {
223         type: 'string',
224         required: false,
225         description: "UUID of the user whose ownership is being transferred to `new_owner_uuid`. You must be an admin to use this option.",
226       },
227       new_user_uuid: {
228         type: 'string',
229         required: false,
230         description: "UUID of the user receiving ownership. You must be an admin to use this option.",
231       }
232     }
233   end
234
235   def self._setup_requires_parameters
236     {
237       uuid: {
238         type: 'string',
239         required: false,
240         description: "UUID of an existing user record to set up."
241       },
242       user: {
243         type: 'object',
244         required: false,
245         description: "Attributes of a new user record to set up.",
246       },
247       repo_name: {
248         type: 'string',
249         required: false,
250         description: "This parameter is obsolete and ignored.",
251       },
252       vm_uuid: {
253         type: 'string',
254         required: false,
255         description: "If given, setup creates a login link to allow this user to access the Arvados virtual machine with this UUID.",
256       },
257       send_notification_email: {
258         type: 'boolean',
259         required: false,
260         default: false,
261         description: "If true, send an email to the user notifying them they can now access this Arvados cluster.",
262       },
263     }
264   end
265
266   def self._update_requires_parameters
267     super.merge({
268       bypass_federation: {
269         type: 'boolean',
270         required: false,
271         default: false,
272         description: "If true, do not try to update the user on any other clusters in the federation,
273 only the cluster that received the request.
274 You must be an administrator to use this flag.",
275       },
276     })
277   end
278
279   def apply_filters(model_class=nil)
280     return super if @read_users.any?(&:is_admin)
281     if params[:uuid] != current_user.andand.uuid
282       # Non-admin index/show returns very basic information about readable users.
283       safe_attrs = ["uuid", "is_active", "is_admin", "is_invited", "email", "first_name", "last_name", "username", "can_write", "can_manage", "kind"]
284       if @select
285         @select = @select & safe_attrs
286       else
287         @select = safe_attrs
288       end
289       @filters += [['is_active', '=', true]]
290     end
291     # This gets called from within find_object_by_uuid.
292     # find_object_by_uuid stores the original value of @select in
293     # @preserve_select, edits the value of @select, calls
294     # find_objects_for_index, then restores @select from the value
295     # of @preserve_select.  So if we want our updated value of
296     # @select here to stick, we have to set @preserve_select.
297     @preserve_select = @select
298     super
299   end
300
301   def nullable_attributes
302     super + [:email, :first_name, :last_name, :username]
303   end
304 end