1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.adminui.endpoint;
23
24 import static java.lang.String.CASE_INSENSITIVE_ORDER;
25 import static org.apache.commons.lang3.StringUtils.trimToEmpty;
26 import static org.apache.commons.lang3.StringUtils.trimToNull;
27 import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
28 import static org.apache.http.HttpStatus.SC_CONFLICT;
29 import static org.apache.http.HttpStatus.SC_CREATED;
30 import static org.apache.http.HttpStatus.SC_FORBIDDEN;
31 import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
32 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
33 import static org.apache.http.HttpStatus.SC_OK;
34 import static org.opencastproject.util.RestUtil.getEndpointUrl;
35 import static org.opencastproject.util.UrlSupport.uri;
36 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
37
38 import org.opencastproject.adminui.util.TextFilter;
39 import org.opencastproject.index.service.resources.list.query.UsersListQuery;
40 import org.opencastproject.index.service.util.RestUtils;
41 import org.opencastproject.security.api.Organization;
42 import org.opencastproject.security.api.Role;
43 import org.opencastproject.security.api.SecurityService;
44 import org.opencastproject.security.api.UnauthorizedException;
45 import org.opencastproject.security.api.User;
46 import org.opencastproject.security.api.UserDirectoryService;
47 import org.opencastproject.security.impl.jpa.JpaOrganization;
48 import org.opencastproject.security.impl.jpa.JpaRole;
49 import org.opencastproject.security.impl.jpa.JpaUser;
50 import org.opencastproject.userdirectory.JpaUserAndRoleProvider;
51 import org.opencastproject.userdirectory.JpaUserReferenceProvider;
52 import org.opencastproject.util.NotFoundException;
53 import org.opencastproject.util.SmartIterator;
54 import org.opencastproject.util.UrlSupport;
55 import org.opencastproject.util.data.Tuple;
56 import org.opencastproject.util.doc.rest.RestParameter;
57 import org.opencastproject.util.doc.rest.RestQuery;
58 import org.opencastproject.util.doc.rest.RestResponse;
59 import org.opencastproject.util.doc.rest.RestService;
60 import org.opencastproject.util.requests.SortCriterion;
61 import org.opencastproject.util.requests.SortCriterion.Order;
62 import org.opencastproject.workflow.api.WorkflowDatabaseException;
63 import org.opencastproject.workflow.api.WorkflowService;
64
65 import com.google.gson.Gson;
66 import com.google.gson.JsonSyntaxException;
67 import com.google.gson.reflect.TypeToken;
68
69 import org.apache.commons.lang3.StringUtils;
70 import org.osgi.service.component.ComponentContext;
71 import org.osgi.service.component.annotations.Activate;
72 import org.osgi.service.component.annotations.Component;
73 import org.osgi.service.component.annotations.Reference;
74 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78 import java.io.IOException;
79 import java.lang.reflect.Type;
80 import java.util.ArrayList;
81 import java.util.Comparator;
82 import java.util.HashMap;
83 import java.util.HashSet;
84 import java.util.Iterator;
85 import java.util.List;
86 import java.util.Map;
87 import java.util.Set;
88 import java.util.stream.Collectors;
89
90 import javax.ws.rs.DELETE;
91 import javax.ws.rs.FormParam;
92 import javax.ws.rs.GET;
93 import javax.ws.rs.POST;
94 import javax.ws.rs.PUT;
95 import javax.ws.rs.Path;
96 import javax.ws.rs.PathParam;
97 import javax.ws.rs.Produces;
98 import javax.ws.rs.QueryParam;
99 import javax.ws.rs.core.MediaType;
100 import javax.ws.rs.core.Response;
101
102 @Path("/admin-ng/users")
103 @RestService(name = "users", title = "User service",
104 abstractText = "Provides operations for users",
105 notes = { "This service offers the default users CRUD Operations for the admin UI.",
106 "<strong>Important:</strong> "
107 + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
108 + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
109 + "DO NOT use this for integration of third-party applications.<em>"})
110 @Component(
111 immediate = true,
112 service = UsersEndpoint.class,
113 property = {
114 "service.description=Admin UI - Users facade Endpoint",
115 "opencast.service.type=org.opencastproject.adminui.endpoint.UsersEndpoint",
116 "opencast.service.path=/admin-ng/users"
117 }
118 )
119 @JaxrsResource
120 public class UsersEndpoint {
121
122
123 private static final Logger logger = LoggerFactory.getLogger(UsersEndpoint.class);
124
125
126 protected UserDirectoryService userDirectoryService;
127
128
129 private JpaUserAndRoleProvider jpaUserAndRoleProvider;
130
131
132 private JpaUserReferenceProvider jpaUserReferenceProvider;
133
134
135 private SecurityService securityService;
136
137
138 private WorkflowService workflowService;
139
140
141 private String endpointBaseUrl;
142
143
144 private static final Type listType = new TypeToken<ArrayList<JsonRole>>() { }.getType();
145 private static final Gson gson = new Gson();
146
147
148
149
150
151
152
153 @Reference
154 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
155 this.userDirectoryService = userDirectoryService;
156 }
157
158
159
160
161
162 @Reference
163 public void setSecurityService(SecurityService securityService) {
164 this.securityService = securityService;
165 }
166
167
168
169
170
171 @Reference
172 public void setJpaUserReferenceProvider(JpaUserReferenceProvider jpaUserReferenceProvider) {
173 this.jpaUserReferenceProvider = jpaUserReferenceProvider;
174 }
175
176
177
178
179
180 @Reference
181 public void setJpaUserAndRoleProvider(JpaUserAndRoleProvider jpaUserAndRoleProvider) {
182 this.jpaUserAndRoleProvider = jpaUserAndRoleProvider;
183 }
184
185
186
187
188
189 @Reference
190 public void setWorkflowService(WorkflowService workflowService) {
191 this.workflowService = workflowService;
192 }
193
194
195 @Activate
196 protected void activate(ComponentContext cc) {
197 logger.info("Activate the Admin ui - Users facade endpoint");
198 final Tuple<String, String> endpointUrl = getEndpointUrl(cc);
199 endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
200 }
201
202 @GET
203 @Path("users.json")
204 @Produces(MediaType.APPLICATION_JSON)
205 @RestQuery(name = "allusers", description = "Returns a list of users", returnDescription = "Returns a JSON representation of the list of user accounts", restParameters = {
206 @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
207 @RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any of the following: STATUS, NAME OR LAST_UPDATED. Add '_DESC' to reverse the sort order (e.g. STATUS_DESC).", type = STRING),
208 @RestParameter(defaultValue = "100", description = "The maximum number of items to return per page.", isRequired = false, name = "limit", type = RestParameter.Type.STRING),
209 @RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset", type = RestParameter.Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "The user accounts.") })
210 public Response getUsers(@QueryParam("filter") String filter, @QueryParam("sort") String sort,
211 @QueryParam("limit") int limit, @QueryParam("offset") int offset) throws IOException {
212 if (limit < 1)
213 limit = 100;
214
215 sort = trimToNull(sort);
216 String filterName = null;
217 String filterRole = null;
218 String filterProvider = null;
219 String filterText = null;
220
221 Map<String, String> filters = RestUtils.parseFilter(filter);
222 for (String name : filters.keySet()) {
223 String value = filters.get(name);
224 if (UsersListQuery.FILTER_NAME_NAME.equals(name)) {
225 filterName = value;
226 } else if (UsersListQuery.FILTER_ROLE_NAME.equals(name)) {
227 filterRole = value;
228 } else if (UsersListQuery.FILTER_PROVIDER_NAME.equals(name)) {
229 filterProvider = value;
230 } else if ((UsersListQuery.FILTER_TEXT_NAME.equals(name)) && (StringUtils.isNotBlank(value))) {
231 filterText = value;
232 }
233 }
234
235
236 List<User> filteredUsers = new ArrayList<>();
237 for (Iterator<User> i = userDirectoryService.getUsers(); i.hasNext();) {
238 User user = i.next();
239
240
241 final String finalFilterRole = filterRole;
242 if (filterName != null && !filterName.equals(user.getName())
243 || (filterRole != null
244 && user.getRoles().stream().noneMatch((r) -> r.getName().equals(finalFilterRole)))
245 || (filterProvider != null
246 && !filterProvider.equals(user.getProvider()))
247 || (filterText != null
248 && !TextFilter.match(filterText, user.getUsername(), user.getName(), user.getEmail(), user.getProvider())
249 && !TextFilter.match(filterText,
250 user.getRoles().stream().map(Role::getName).collect(Collectors.joining(" "))))) {
251 continue;
252 }
253 filteredUsers.add(user);
254 }
255 int total = filteredUsers.size();
256
257
258 if (sort != null) {
259 final ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
260 filteredUsers.sort((user1, user2) -> {
261 for (SortCriterion criterion : sortCriteria) {
262 Order order = criterion.getOrder();
263 switch (criterion.getFieldName()) {
264 case "name":
265 if (order.equals(Order.Descending))
266 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user2.getName()), trimToEmpty(user1.getName()));
267 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user1.getName()), trimToEmpty(user2.getName()));
268 case "username":
269 if (order.equals(Order.Descending))
270 return CASE_INSENSITIVE_ORDER
271 .compare(trimToEmpty(user2.getUsername()), trimToEmpty(user1.getUsername()));
272 return CASE_INSENSITIVE_ORDER
273 .compare(trimToEmpty(user1.getUsername()), trimToEmpty(user2.getUsername()));
274 case "email":
275 if (order.equals(Order.Descending))
276 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user2.getEmail()), trimToEmpty(user1.getEmail()));
277 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user1.getEmail()), trimToEmpty(user2.getEmail()));
278 case "roles":
279 String roles1 = user1.getRoles().stream().map(Role::getName).collect(Collectors.joining(","));
280 String roles2 = user1.getRoles().stream().map(Role::getName).collect(Collectors.joining(","));
281 if (order.equals(Order.Descending))
282 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(roles2), trimToEmpty(roles1));
283 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(roles1), trimToEmpty(roles2));
284 case "provider":
285 if (order.equals(Order.Descending))
286 return CASE_INSENSITIVE_ORDER
287 .compare(trimToEmpty(user2.getProvider()), trimToEmpty(user1.getProvider()));
288 return CASE_INSENSITIVE_ORDER
289 .compare(trimToEmpty(user1.getProvider()), trimToEmpty(user2.getProvider()));
290 default:
291 logger.info("Unknown sort type: {}", criterion.getFieldName());
292 return 0;
293 }
294 }
295 return 0;
296 });
297 }
298
299
300 filteredUsers = new SmartIterator<User>(limit, offset).applyLimitAndOffset(filteredUsers);
301
302 List<Map<String, Object>> usersJSON = new ArrayList<>();
303 for (User user : filteredUsers) {
304 usersJSON.add(generateJsonUser(user));
305 }
306
307 Map<String, Object> response = Map.of(
308 "results", usersJSON,
309 "count", usersJSON.size(),
310 "offset", offset,
311 "limit", limit,
312 "total", total);
313 return Response.ok(gson.toJson(response)).build();
314 }
315
316 @POST
317 @Path("/")
318 @RestQuery(name = "createUser", description = "Create a new user", returnDescription = "The location of the new ressource", restParameters = {
319 @RestParameter(description = "The username.", isRequired = true, name = "username", type = STRING),
320 @RestParameter(description = "The password.", isRequired = true, name = "password", type = STRING),
321 @RestParameter(description = "The name.", isRequired = false, name = "name", type = STRING),
322 @RestParameter(description = "The email.", isRequired = false, name = "email", type = STRING),
323 @RestParameter(name = "roles", type = STRING, isRequired = false, description = "The user roles as a json array, e.g. <br>"
324 + "[{'name': 'ROLE_ADMIN', 'type': 'INTERNAL'}, {'name': 'ROLE_XY', 'type': 'INTERNAL'}]") },
325 responses = {
326 @RestResponse(responseCode = SC_CREATED, description = "User has been created."),
327 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to create a user with a admin role."),
328 @RestResponse(responseCode = SC_CONFLICT, description = "An user with this username already exist.")})
329 public Response createUser(@FormParam("username") String username, @FormParam("password") String password,
330 @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
331
332 if (StringUtils.isBlank(username)) {
333 return Response.status(SC_BAD_REQUEST).entity("Missing username").build();
334 }
335 if (StringUtils.isBlank(password)) {
336 return Response.status(SC_BAD_REQUEST).entity("Missing password").build();
337 }
338
339 User existingUser = jpaUserAndRoleProvider.loadUser(username);
340 if (existingUser != null) {
341 return Response.status(SC_CONFLICT).build();
342 }
343
344 JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
345 Set<JpaRole> rolesSet;
346 try {
347 rolesSet = parseJsonRoles(roles);
348 } catch (IllegalArgumentException e) {
349 logger.debug("Received invalid JSON for roles", e);
350 return Response.status(SC_BAD_REQUEST).entity("Invalid JSON for roles").build();
351 }
352
353 if (rolesSet == null) {
354 rolesSet = new HashSet<>();
355 rolesSet.add(new JpaRole(organization.getAnonymousRole(), organization));
356 }
357
358 JpaUser user = new JpaUser(username, password, organization, name, email, jpaUserAndRoleProvider.getName(), true,
359 rolesSet);
360 try {
361 jpaUserAndRoleProvider.addUser(user);
362 return Response.created(uri(endpointBaseUrl, user.getUsername() + ".json")).build();
363 } catch (UnauthorizedException e) {
364 return Response.status(SC_FORBIDDEN).build();
365 }
366 }
367
368 @GET
369 @Path("{username}.json")
370 @RestQuery(name = "getUser", description = "Get an user", returnDescription = "Status ok", pathParameters = @RestParameter(name = "username", type = STRING, isRequired = true, description = "The username"), responses = {
371 @RestResponse(responseCode = SC_OK, description = "User has been found."),
372 @RestResponse(responseCode = SC_NOT_FOUND, description = "User not found.") })
373 public Response getUser(@PathParam("username") String username) {
374
375 User user = userDirectoryService.loadUser(username);
376 if (user == null) {
377 return Response.status(SC_NOT_FOUND).build();
378 }
379
380 return Response.ok(gson.toJson(generateJsonUser(user))).build();
381 }
382
383 @PUT
384 @Path("{username}.json")
385 @RestQuery(name = "updateUser", description = "Update an user", returnDescription = "Status ok", restParameters = {
386 @RestParameter(description = "The password.", isRequired = false, name = "password", type = STRING),
387 @RestParameter(description = "The name.", isRequired = false, name = "name", type = STRING),
388 @RestParameter(description = "The email.", isRequired = false, name = "email", type = STRING),
389 @RestParameter(name = "roles", type = STRING, isRequired = false, description = "The user roles as a json array") }, pathParameters = @RestParameter(name = "username", type = STRING, isRequired = true, description = "The username"), responses = {
390 @RestResponse(responseCode = SC_OK, description = "User has been updated."),
391 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to update a user with admin role."),
392 @RestResponse(responseCode = SC_BAD_REQUEST, description = "Invalid data provided.")})
393 public Response updateUser(@PathParam("username") String username, @FormParam("password") String password,
394 @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
395
396 User user = jpaUserAndRoleProvider.loadUser(username);
397 if (user == null) {
398 return createUser(username, password, name, email, roles);
399 }
400
401 Set<JpaRole> rolesSet;
402 try {
403 rolesSet = parseJsonRoles(roles);
404 } catch (IllegalArgumentException e) {
405 logger.debug("Received invalid JSON for roles", e);
406 return Response.status(SC_BAD_REQUEST).build();
407 }
408
409 JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
410 if (rolesSet == null) {
411
412 rolesSet = new HashSet<>();
413 for (Role role : user.getRoles()) {
414 rolesSet.add(new JpaRole(role.getName(), organization, role.getDescription(), role.getType()));
415 }
416 }
417
418 try {
419 jpaUserAndRoleProvider.updateUser(new JpaUser(username, password, organization, name, email,
420 jpaUserAndRoleProvider.getName(), true, rolesSet));
421 userDirectoryService.invalidate(username);
422 return Response.status(SC_OK).build();
423 } catch (UnauthorizedException ex) {
424 return Response.status(Response.Status.FORBIDDEN).build();
425 } catch (NotFoundException e) {
426 return Response.serverError().build();
427 }
428 }
429
430 @DELETE
431 @Path("{username}.json")
432 @RestQuery(name = "deleteUser", description = "Deleter a new user", returnDescription = "Status ok", pathParameters = @RestParameter(name = "username", type = STRING, isRequired = true, description = "The username"), responses = {
433 @RestResponse(responseCode = SC_OK, description = "User has been deleted."),
434 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to delete a user with admin role."),
435 @RestResponse(responseCode = SC_NOT_FOUND, description = "User not found.") })
436 public Response deleteUser(@PathParam("username") String username) throws NotFoundException {
437 Organization organization = securityService.getOrganization();
438 boolean userReferenceNotFound = false;
439 boolean userNotFound = false;
440
441 try {
442 if (workflowService.userHasActiveWorkflows(username)) {
443 logger.debug("Workflow still active for user {}:", username);
444 return Response.status(SC_CONFLICT).build();
445 }
446 } catch (WorkflowDatabaseException e) {
447 logger.error("Error during deletion of user {}", username, e);
448 return Response.status(SC_INTERNAL_SERVER_ERROR).build();
449 }
450
451 try {
452 try {
453 jpaUserReferenceProvider.deleteUser(username, organization.getId());
454 } catch (NotFoundException e) {
455 userReferenceNotFound = true;
456 }
457 try {
458 jpaUserAndRoleProvider.deleteUser(username, organization.getId());
459 } catch (NotFoundException e) {
460 userNotFound = true;
461 }
462
463 if (userNotFound && userReferenceNotFound) {
464 throw new NotFoundException();
465 }
466
467 userDirectoryService.invalidate(username);
468 } catch (NotFoundException e) {
469 logger.debug("User {} not found.", username);
470 return Response.status(SC_NOT_FOUND).build();
471 } catch (UnauthorizedException e) {
472 return Response.status(SC_FORBIDDEN).build();
473 } catch (Exception e) {
474 logger.error("Error during deletion of user {}", username, e);
475 return Response.status(SC_INTERNAL_SERVER_ERROR).build();
476 }
477
478 logger.debug("User {} removed.", username);
479 return Response.status(SC_OK).build();
480 }
481
482
483
484
485
486
487
488
489
490
491 private Set<JpaRole> parseJsonRoles(final String roles) throws IllegalArgumentException {
492 List<JsonRole> rolesList;
493 try {
494 rolesList = gson.fromJson(roles, listType);
495 } catch (JsonSyntaxException e) {
496 throw new IllegalArgumentException(e);
497 }
498 if (rolesList == null) {
499 return null;
500 }
501
502 JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
503 Set<JpaRole> rolesSet = new HashSet<>();
504 for (JsonRole role: rolesList) {
505 try {
506 rolesSet.add(new JpaRole(role.getName(), organization, null, role.getType()));
507 } catch (NullPointerException e) {
508 throw new IllegalArgumentException(e);
509 }
510 }
511 return rolesSet;
512 }
513
514 private Map<String, Object> generateJsonUser(User user) {
515
516 Map<String, Object> userData = new HashMap<>();
517 userData.put("username", user.getUsername());
518 userData.put("manageable", user.isManageable());
519 userData.put("name", user.getName());
520 userData.put("email", user.getEmail());
521 userData.put("provider", user.getProvider());
522 userData.put("roles", user.getRoles().stream()
523 .sorted(Comparator.comparing(Role::getName))
524 .map((r) -> new JsonRole(r.getName(), r.getType()))
525 .collect(Collectors.toList()));
526 return userData;
527 }
528
529 class JsonRole {
530 private String name;
531 private String type;
532
533 JsonRole(String name, Role.Type type) {
534 this.name = name;
535 this.type = type.toString();
536 }
537
538 public String getName() {
539 return name;
540 }
541
542 public Role.Type getType() {
543 return Role.Type.valueOf(type);
544 }
545 }
546
547 }