View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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   /** The logging facility */
126   private static final Logger logger = LoggerFactory.getLogger(UsersEndpoint.class);
127 
128   /** The global user directory service */
129   protected UserDirectoryService userDirectoryService;
130 
131   /** The internal role and user provider */
132   private JpaUserAndRoleProvider jpaUserAndRoleProvider;
133 
134   /** The internal user reference provider */
135   private JpaUserReferenceProvider jpaUserReferenceProvider;
136 
137   /** The security service */
138   private SecurityService securityService;
139 
140   /** The workflow service */
141   private WorkflowService workflowService;
142 
143   /** Base url of this endpoint */
144   private String endpointBaseUrl;
145 
146   /** For JSON serialization */
147   private static final Type listType = new TypeToken<ArrayList<JsonRole>>() { }.getType();
148   private static final Gson gson = new Gson();
149 
150   /**
151    * Sets the user directory service
152    *
153    * @param userDirectoryService
154    *          the userDirectoryService to set
155    */
156   @Reference
157   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
158     this.userDirectoryService = userDirectoryService;
159   }
160 
161   /**
162    * @param securityService
163    *          the securityService to set
164    */
165   @Reference
166   public void setSecurityService(SecurityService securityService) {
167     this.securityService = securityService;
168   }
169 
170   /**
171    * @param jpaUserReferenceProvider
172    *          the user provider to set
173    */
174   @Reference
175   public void setJpaUserReferenceProvider(JpaUserReferenceProvider jpaUserReferenceProvider) {
176     this.jpaUserReferenceProvider = jpaUserReferenceProvider;
177   }
178 
179   /**
180    * @param jpaUserAndRoleProvider
181    *          the user provider to set
182    */
183   @Reference
184   public void setJpaUserAndRoleProvider(JpaUserAndRoleProvider jpaUserAndRoleProvider) {
185     this.jpaUserAndRoleProvider = jpaUserAndRoleProvider;
186   }
187 
188   /**
189    * @param workflowService
190    *          the user provider to set
191    */
192   @Reference
193   public void setWorkflowService(WorkflowService workflowService) {
194     this.workflowService = workflowService;
195   }
196 
197   /** OSGi callback. */
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     // Filter users by filter criteria
253     List<User> filteredUsers = new ArrayList<>();
254     for (User user : userDirectoryService.getUsers()) {
255       // Filter list
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     // Sort by name, description or role
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     // Apply Limit and offset
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       //  use the previous roles if no new ones are provided
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    * Parse a JSON roles string.
571    *
572    * @param roles
573    *          Array of roles as JSON strings.
574    * @return Set of roles or null
575    * @throws IllegalArgumentException
576    *          Invalid JSON data
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     // Prepare the roles
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 }