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