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.userdirectory.endpoint;
23
24 import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
25 import static org.apache.http.HttpStatus.SC_CONFLICT;
26 import static org.apache.http.HttpStatus.SC_CREATED;
27 import static org.apache.http.HttpStatus.SC_FORBIDDEN;
28 import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
29 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
30 import static org.apache.http.HttpStatus.SC_NO_CONTENT;
31 import static org.apache.http.HttpStatus.SC_OK;
32 import static org.opencastproject.util.RestUtil.getEndpointUrl;
33 import static org.opencastproject.util.UrlSupport.uri;
34 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
35
36 import org.opencastproject.security.api.JaxbUser;
37 import org.opencastproject.security.api.JaxbUserList;
38 import org.opencastproject.security.api.SecurityService;
39 import org.opencastproject.security.api.UnauthorizedException;
40 import org.opencastproject.security.api.User;
41 import org.opencastproject.security.impl.jpa.JpaOrganization;
42 import org.opencastproject.security.impl.jpa.JpaRole;
43 import org.opencastproject.security.impl.jpa.JpaUser;
44 import org.opencastproject.userdirectory.JpaUserAndRoleProvider;
45 import org.opencastproject.util.NotFoundException;
46 import org.opencastproject.util.UrlSupport;
47 import org.opencastproject.util.data.Tuple;
48 import org.opencastproject.util.doc.rest.RestParameter;
49 import org.opencastproject.util.doc.rest.RestQuery;
50 import org.opencastproject.util.doc.rest.RestResponse;
51 import org.opencastproject.util.doc.rest.RestService;
52
53 import com.google.gson.Gson;
54 import com.google.gson.JsonSyntaxException;
55 import com.google.gson.reflect.TypeToken;
56
57 import org.apache.commons.lang3.StringUtils;
58 import org.osgi.service.component.ComponentContext;
59 import org.osgi.service.component.annotations.Component;
60 import org.osgi.service.component.annotations.Reference;
61 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import java.io.IOException;
66 import java.lang.reflect.Type;
67 import java.util.ArrayList;
68 import java.util.Collections;
69 import java.util.HashSet;
70 import java.util.Iterator;
71 import java.util.List;
72 import java.util.Objects;
73 import java.util.Set;
74 import java.util.stream.Collectors;
75
76 import javax.persistence.RollbackException;
77 import javax.ws.rs.Consumes;
78 import javax.ws.rs.DELETE;
79 import javax.ws.rs.FormParam;
80 import javax.ws.rs.GET;
81 import javax.ws.rs.POST;
82 import javax.ws.rs.PUT;
83 import javax.ws.rs.Path;
84 import javax.ws.rs.PathParam;
85 import javax.ws.rs.Produces;
86 import javax.ws.rs.QueryParam;
87 import javax.ws.rs.core.MediaType;
88 import javax.ws.rs.core.Response;
89
90
91
92
93 @Path("/user-utils")
94 @RestService(
95 name = "UsersUtils",
96 title = "User utils",
97 notes = "This service offers the default CRUD Operations for the internal Opencast users.",
98 abstractText = "Provides operations for internal Opencast users")
99 @Component(
100 property = {
101 "service.description=User REST endpoint",
102 "opencast.service.type=org.opencastproject.userdirectory.endpoint.UserEndpoint",
103 "opencast.service.path=/user-utils",
104 "opencast.service.jobproducer=false"
105 },
106 immediate = true,
107 service = { UserEndpoint.class }
108 )
109 @JaxrsResource
110 public class UserEndpoint {
111
112
113 private static final Logger logger = LoggerFactory.getLogger(UserEndpoint.class);
114
115 private JpaUserAndRoleProvider jpaUserAndRoleProvider;
116
117 private SecurityService securityService;
118
119 private String endpointBaseUrl;
120
121 private static final Gson gson = new Gson();
122
123 private class UserData {
124 private String username;
125 private String password;
126 private String name;
127 private String email;
128 private Set<String> roles;
129 }
130
131 private final Type userListType = new TypeToken<List<User>>() { }.getType();
132
133
134 public void activate(ComponentContext cc) {
135 logger.info("Start users endpoint");
136 final Tuple<String, String> endpointUrl = getEndpointUrl(cc);
137 endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
138 }
139
140
141
142
143
144 @Reference
145 public void setSecurityService(SecurityService securityService) {
146 this.securityService = securityService;
147 }
148
149
150
151
152
153 @Reference
154 public void setJpaUserAndRoleProvider(JpaUserAndRoleProvider jpaUserAndRoleProvider) {
155 this.jpaUserAndRoleProvider = jpaUserAndRoleProvider;
156 }
157
158 @GET
159 @Path("users.json")
160 @Produces(MediaType.APPLICATION_JSON)
161 @RestQuery(
162 name = "allusersasjson",
163 description = "Returns a list of users",
164 returnDescription = "Returns a JSON representation of the list of user accounts",
165 restParameters = {
166 @RestParameter(
167 name = "limit",
168 defaultValue = "100",
169 description = "The maximum number of items to return per page.",
170 isRequired = false,
171 type = RestParameter.Type.STRING),
172 @RestParameter(
173 name = "offset",
174 defaultValue = "0",
175 description = "The page number.",
176 isRequired = false,
177 type = RestParameter.Type.STRING)
178 }, responses = {
179 @RestResponse(
180 responseCode = SC_OK,
181 description = "The user accounts.")
182 })
183 public JaxbUserList getUsersAsJson(@QueryParam("limit") int limit, @QueryParam("offset") int offset)
184 throws IOException {
185
186
187 if (limit < 1) {
188 limit = 100;
189 }
190
191 JaxbUserList userList = new JaxbUserList();
192 for (Iterator<User> i = jpaUserAndRoleProvider.findUsers("%", offset, limit); i.hasNext();) {
193 userList.add(i.next());
194 }
195 return userList;
196 }
197
198 @GET
199 @Path("{username}.json")
200 @Produces(MediaType.APPLICATION_JSON)
201 @RestQuery(
202 name = "user",
203 description = "Returns a user",
204 returnDescription = "Returns a JSON representation of a user",
205 pathParameters = {
206 @RestParameter(
207 name = "username",
208 description = "The username.",
209 isRequired = true,
210 type = STRING)
211 }, responses = {
212 @RestResponse(
213 responseCode = SC_OK,
214 description = "The user account."),
215 @RestResponse(
216 responseCode = SC_NOT_FOUND,
217 description = "User not found")
218 })
219 public Response getUserAsJson(@PathParam("username") String username) throws NotFoundException {
220 User user = jpaUserAndRoleProvider.loadUser(username);
221 if (user == null) {
222 logger.debug("Requested user not found: {}", username);
223 return Response.status(SC_NOT_FOUND).build();
224 }
225 return Response.ok(JaxbUser.fromUser(user)).build();
226 }
227
228 @GET
229 @Path("users/md5.json")
230 @Produces(MediaType.APPLICATION_JSON)
231 @RestQuery(
232 name = "users-with-insecure-hashing",
233 description = "Returns a list of users which passwords are stored using MD5 hashes",
234 returnDescription = "Returns a JSON representation of the list of matching user accounts",
235 responses = {
236 @RestResponse(
237 responseCode = SC_OK,
238 description = "The user accounts.")
239 })
240 public JaxbUserList getUserWithInsecurePasswordHashingAsJson() {
241 JaxbUserList userList = new JaxbUserList();
242 for (User user: jpaUserAndRoleProvider.findInsecurePasswordHashes()) {
243 userList.add(user);
244 }
245 return userList;
246 }
247
248 @POST
249 @Path("/")
250 @RestQuery(
251 name = "createUser",
252 description = "Create a new user",
253 returnDescription = "Location of the new resource",
254 restParameters = {
255 @RestParameter(
256 name = "username",
257 description = "The username.",
258 isRequired = true,
259 type = STRING),
260 @RestParameter(
261 name = "password",
262 description = "The password.",
263 isRequired = true,
264 type = STRING),
265 @RestParameter(
266 name = "name",
267 description = "The name.",
268 isRequired = false,
269 type = STRING),
270 @RestParameter(
271 name = "email",
272 description = "The email.",
273 isRequired = false,
274 type = STRING),
275 @RestParameter(
276 name = "roles",
277 description = "The user roles as a json array, for example: [\"ROLE_USER\", \"ROLE_ADMIN\"]",
278 isRequired = false,
279 type = STRING)
280 }, responses = {
281 @RestResponse(
282 responseCode = SC_BAD_REQUEST,
283 description = "Malformed request syntax."),
284 @RestResponse(
285 responseCode = SC_CREATED,
286 description = "User has been created."),
287 @RestResponse(
288 responseCode = SC_CONFLICT,
289 description = "An user with this username already exist."),
290 @RestResponse(
291 responseCode = SC_FORBIDDEN,
292 description = "Not enough permissions to create a user with the admin role.")
293 })
294 public Response createUser(@FormParam("username") String username, @FormParam("password") String password,
295 @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
296
297 if (jpaUserAndRoleProvider.loadUser(username) != null) {
298 return Response.status(SC_CONFLICT).build();
299 }
300
301 try {
302 Set<JpaRole> rolesSet = parseRoles(roles);
303
304
305 logger.debug("Updating user {}", username);
306 JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
307 JpaUser user = new JpaUser(username, password, organization, name, email, jpaUserAndRoleProvider.getName(), true,
308 rolesSet);
309 try {
310 jpaUserAndRoleProvider.addUser(user);
311 return Response.created(uri(endpointBaseUrl, user.getUsername() + ".json")).build();
312 } catch (UnauthorizedException ex) {
313 logger.debug("Create user failed", ex);
314 return Response.status(Response.Status.FORBIDDEN).build();
315 }
316
317 } catch (IllegalArgumentException e) {
318 logger.debug("Request with malformed ROLE data: {}", roles);
319 return Response.status(SC_BAD_REQUEST).build();
320 }
321 }
322
323 @POST
324 @Path("/users.json")
325 @Consumes(MediaType.APPLICATION_JSON)
326 @RestQuery(
327 name = "createUsers",
328 description = "Create a list of new users",
329 returnDescription = "If the operation succeeded or not",
330 responses = {
331 @RestResponse(
332 responseCode = SC_BAD_REQUEST,
333 description = "Malformed request syntax."),
334 @RestResponse(
335 responseCode = SC_CONFLICT,
336 description = "At least one user already existed."),
337 @RestResponse(
338 responseCode = SC_NO_CONTENT,
339 description = "Users have been created.") })
340 public Response createUsers(String body) throws UnauthorizedException {
341 logger.debug("Provided user JSON: {}", body);
342 try {
343 for (var user: parseUserData(body)) {
344 jpaUserAndRoleProvider.addUser(user);
345 logger.info("Created user {}", user.getUsername());
346 }
347 } catch (RollbackException e) {
348 logger.debug("Error storing user in database", e);
349 return Response.status(Response.Status.CONFLICT).build();
350 } catch (IllegalArgumentException e) {
351 logger.debug("Error parsing the provided JSON body", e);
352 return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
353 }
354 return Response.status(Response.Status.CREATED).build();
355 }
356
357 @PUT
358 @Path("{username}.json")
359 @RestQuery(
360 name = "updateUser",
361 description = "Update an user",
362 returnDescription = "Status ok",
363 restParameters = {
364 @RestParameter(
365 name = "password",
366 description = "The password.",
367 isRequired = true,
368 type = STRING),
369 @RestParameter(
370 name = "name",
371 description = "The name.",
372 isRequired = false,
373 type = STRING),
374 @RestParameter(
375 name = "email",
376 description = "The email.",
377 isRequired = false,
378 type = STRING),
379 @RestParameter(
380 name = "roles",
381 description = "The user roles as a json array, for example: [\"ROLE_USER\", \"ROLE_ADMIN\"]",
382 isRequired = false,
383 type = STRING)
384 }, pathParameters = @RestParameter(
385 name = "username",
386 description = "The username",
387 isRequired = true,
388 type = STRING),
389 responses = {
390 @RestResponse(
391 responseCode = SC_BAD_REQUEST,
392 description = "Malformed request syntax."),
393 @RestResponse(
394 responseCode = SC_FORBIDDEN,
395 description = "Not enough permissions to update a user with the admin role."),
396 @RestResponse(
397 responseCode = SC_OK,
398 description = "User has been updated.") })
399 public Response setUser(@PathParam("username") String username, @FormParam("password") String password,
400 @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
401
402 try {
403 User user = jpaUserAndRoleProvider.loadUser(username);
404 if (user == null) {
405 return createUser(username, password, name, email, roles);
406 }
407
408 Set<JpaRole> rolesSet = parseRoles(roles);
409
410 logger.debug("Updating user {}", username);
411 JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
412 jpaUserAndRoleProvider.updateUser(new JpaUser(username, password, organization, name, email,
413 jpaUserAndRoleProvider.getName(), true, rolesSet));
414 return Response.status(SC_OK).build();
415 } catch (NotFoundException e) {
416 logger.debug("User {} not found.", username);
417 return Response.status(SC_NOT_FOUND).build();
418 } catch (UnauthorizedException e) {
419 logger.debug("Update user failed", e);
420 return Response.status(Response.Status.FORBIDDEN).build();
421 } catch (IllegalArgumentException e) {
422 logger.debug("Request with malformed ROLE data: {}", roles);
423 return Response.status(SC_BAD_REQUEST).build();
424 }
425 }
426
427 @DELETE
428 @Path("{username}.json")
429 @RestQuery(
430 name = "deleteUser",
431 description = "Delete a new user",
432 returnDescription = "Status ok",
433 pathParameters = @RestParameter(
434 name = "username",
435 type = STRING,
436 isRequired = true,
437 description = "The username"),
438 responses = {
439 @RestResponse(
440 responseCode = SC_OK,
441 description = "User has been deleted."),
442 @RestResponse(
443 responseCode = SC_FORBIDDEN,
444 description = "Not enough permissions to delete a user with the admin role."),
445 @RestResponse(
446 responseCode = SC_NOT_FOUND,
447 description = "User not found.")
448 })
449 public Response deleteUser(@PathParam("username") String username) {
450 try {
451 jpaUserAndRoleProvider.deleteUser(username, securityService.getOrganization().getId());
452 } catch (NotFoundException e) {
453 logger.debug("User {} not found.", username);
454 return Response.status(SC_NOT_FOUND).build();
455 } catch (UnauthorizedException e) {
456 logger.debug("Error during deletion of user {}", username, e);
457 return Response.status(SC_FORBIDDEN).build();
458 } catch (Exception e) {
459 logger.error("Error during deletion of user {}", username, e);
460 return Response.status(SC_INTERNAL_SERVER_ERROR).build();
461 }
462
463 logger.debug("User {} removed.", username);
464 return Response.status(SC_OK).build();
465 }
466
467
468
469
470
471
472
473 private Set<JpaRole> parseRoles(String roles) throws IllegalArgumentException {
474
475 final String[] rolesArray;
476 try {
477 rolesArray = gson.fromJson(StringUtils.isEmpty(roles) ? "[]" : roles, String[].class);
478 } catch (JsonSyntaxException e) {
479 throw new IllegalArgumentException("Error parsing JSON array", e);
480 }
481
482 Set<JpaRole> rolesSet = new HashSet<>();
483
484 for (var role : rolesArray) {
485 rolesSet.add(new JpaRole(role, (JpaOrganization) securityService.getOrganization()));
486 }
487
488 return rolesSet;
489 }
490
491 private List<JpaUser> parseUserData(String userJson) {
492 final UserData[] userArray;
493 try {
494 userArray = gson.fromJson(userJson, UserData[].class);
495 } catch (JsonSyntaxException e) {
496 throw new IllegalArgumentException("Error parsing JSON array", e);
497 }
498
499 if (Objects.isNull(userArray)) {
500 throw new IllegalArgumentException("The JSON may not be empty or `null`");
501 }
502
503 var users = new ArrayList<JpaUser>(userArray.length);
504 var organization = (JpaOrganization) securityService.getOrganization();
505 var provider = jpaUserAndRoleProvider.getName();
506 for (var u: userArray) {
507 if (Objects.isNull(u.username)) {
508 throw new IllegalArgumentException("Field `username` may not be `null`");
509 }
510
511 Set<JpaRole> roles = Objects.isNull(u.roles)
512 ? Collections.emptySet()
513 : u.roles.stream().map(r -> new JpaRole(r, organization)).collect(Collectors.toSet());
514 users.add(new JpaUser(u.username, u.password, organization, u.name, u.email, provider, true, roles));
515 }
516 return users;
517 }
518
519 }