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(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   /** The logging facility */
124   private static final Logger logger = LoggerFactory.getLogger(UsersEndpoint.class);
125 
126   /** The global user directory service */
127   protected UserDirectoryService userDirectoryService;
128 
129   /** The internal role and user provider */
130   private JpaUserAndRoleProvider jpaUserAndRoleProvider;
131 
132   /** The internal user reference provider */
133   private JpaUserReferenceProvider jpaUserReferenceProvider;
134 
135   /** The security service */
136   private SecurityService securityService;
137 
138   /** The workflow service */
139   private WorkflowService workflowService;
140 
141   /** Base url of this endpoint */
142   private String endpointBaseUrl;
143 
144   /** For JSON serialization */
145   private static final Type listType = new TypeToken<ArrayList<JsonRole>>() { }.getType();
146   private static final Gson gson = new Gson();
147 
148   /**
149    * Sets the user directory service
150    *
151    * @param userDirectoryService
152    *          the userDirectoryService to set
153    */
154   @Reference
155   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
156     this.userDirectoryService = userDirectoryService;
157   }
158 
159   /**
160    * @param securityService
161    *          the securityService to set
162    */
163   @Reference
164   public void setSecurityService(SecurityService securityService) {
165     this.securityService = securityService;
166   }
167 
168   /**
169    * @param jpaUserReferenceProvider
170    *          the user provider to set
171    */
172   @Reference
173   public void setJpaUserReferenceProvider(JpaUserReferenceProvider jpaUserReferenceProvider) {
174     this.jpaUserReferenceProvider = jpaUserReferenceProvider;
175   }
176 
177   /**
178    * @param jpaUserAndRoleProvider
179    *          the user provider to set
180    */
181   @Reference
182   public void setJpaUserAndRoleProvider(JpaUserAndRoleProvider jpaUserAndRoleProvider) {
183     this.jpaUserAndRoleProvider = jpaUserAndRoleProvider;
184   }
185 
186   /**
187    * @param workflowService
188    *          the user provider to set
189    */
190   @Reference
191   public void setWorkflowService(WorkflowService workflowService) {
192     this.workflowService = workflowService;
193   }
194 
195   /** OSGi callback. */
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     // Filter users by filter criteria
237     List<User> filteredUsers = new ArrayList<>();
238     for (User user : userDirectoryService.getUsers()) {
239       // Filter list
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     // Sort by name, description or role
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     // Apply Limit and offset
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       //  use the previous roles if no new ones are provided
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    * Parse a JSON roles string.
515    *
516    * @param roles
517    *          Array of roles as JSON strings.
518    * @return Set of roles or null
519    * @throws IllegalArgumentException
520    *          Invalid JSON data
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     // Prepare the roles
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 }