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