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.Math.max;
25 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
26 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
27 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
28 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
29 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
30 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
31 import static javax.servlet.http.HttpServletResponse.SC_OK;
32 import static org.opencastproject.index.service.util.JSONUtils.safeString;
33 import static org.opencastproject.index.service.util.RestUtils.okJsonList;
34 import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
35 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
36 import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
37
38 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
39 import org.opencastproject.index.service.api.IndexService;
40 import org.opencastproject.index.service.resources.list.query.GroupsListQuery;
41 import org.opencastproject.index.service.util.RestUtils;
42 import org.opencastproject.security.api.SecurityService;
43 import org.opencastproject.security.api.UnauthorizedException;
44 import org.opencastproject.security.api.User;
45 import org.opencastproject.security.api.UserDirectoryService;
46 import org.opencastproject.security.impl.jpa.JpaGroup;
47 import org.opencastproject.userdirectory.ConflictException;
48 import org.opencastproject.userdirectory.JpaGroupRoleProvider;
49 import org.opencastproject.util.NotFoundException;
50 import org.opencastproject.util.doc.rest.RestParameter;
51 import org.opencastproject.util.doc.rest.RestQuery;
52 import org.opencastproject.util.doc.rest.RestResponse;
53 import org.opencastproject.util.doc.rest.RestService;
54 import org.opencastproject.util.requests.SortCriterion;
55
56 import com.google.gson.JsonArray;
57 import com.google.gson.JsonObject;
58
59 import org.apache.commons.lang3.StringUtils;
60 import org.osgi.service.component.ComponentContext;
61 import org.osgi.service.component.annotations.Activate;
62 import org.osgi.service.component.annotations.Component;
63 import org.osgi.service.component.annotations.Reference;
64 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 import java.io.IOException;
69 import java.util.ArrayList;
70 import java.util.HashMap;
71 import java.util.Iterator;
72 import java.util.List;
73 import java.util.Map;
74 import java.util.Objects;
75 import java.util.Optional;
76 import java.util.stream.Collectors;
77
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.WebApplicationException;
88 import javax.ws.rs.core.MediaType;
89 import javax.ws.rs.core.Response;
90 import javax.ws.rs.core.Response.Status;
91
92 @Path("/admin-ng/groups")
93 @RestService(name = "groups", title = "Group service",
94 abstractText = "Provides operations for groups",
95 notes = { "This service offers the default groups CRUD operations for the admin interface.",
96 "<strong>Important:</strong> "
97 + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
98 + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
99 + "DO NOT use this for integration of third-party applications.<em>"})
100 @Component(
101 immediate = true,
102 service = GroupsEndpoint.class,
103 property = {
104 "service.description=Admin UI - Groups Endpoint",
105 "opencast.service.type=org.opencastproject.adminui.GroupsEndpoint",
106 "opencast.service.path=/admin-ng/groups",
107 }
108 )
109 @JaxrsResource
110 public class GroupsEndpoint {
111
112
113 private static final Logger logger = LoggerFactory.getLogger(GroupsEndpoint.class);
114
115
116 private ElasticsearchIndex searchIndex;
117
118
119 private SecurityService securityService;
120
121
122 private UserDirectoryService userDirectoryService;
123
124
125 private IndexService indexService;
126
127
128 private JpaGroupRoleProvider jpaGroupRoleProvider;
129
130
131 @Reference
132 public void setSecurityService(SecurityService securityService) {
133 this.securityService = securityService;
134 }
135
136
137 @Reference
138 public void setIndexService(IndexService indexService) {
139 this.indexService = indexService;
140 }
141
142
143 @Reference
144 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
145 this.userDirectoryService = userDirectoryService;
146 }
147
148
149 @Reference
150 public void setSearchIndex(ElasticsearchIndex searchIndex) {
151 this.searchIndex = searchIndex;
152 }
153
154
155 @Reference
156 public void setGroupRoleProvider(JpaGroupRoleProvider jpaGroupRoleProvider) {
157 this.jpaGroupRoleProvider = jpaGroupRoleProvider;
158 }
159
160
161 @Activate
162 protected void activate(ComponentContext cc) {
163 logger.info("Activate the Admin ui - Groups facade endpoint");
164 }
165
166 @GET
167 @Produces(MediaType.APPLICATION_JSON)
168 @Path("groups.json")
169 @RestQuery(
170 name = "allgroupsasjson",
171 description = "Returns a list of groups",
172 returnDescription = "List of groups for the current user's organization as JSON.",
173 restParameters = {
174 @RestParameter(name = "filter", isRequired = false, type = STRING,
175 description = "Filter used for the query, formatted like: 'filter1:value1,filter2:value2'"),
176 @RestParameter(name = "sort", isRequired = false, type = STRING,
177 description = "The sort order. May include any of the following: NAME, DESCRIPTION, ROLE. "
178 + "Add '_DESC' to reverse the sort order (e.g. NAME_DESC)."),
179 @RestParameter(name = "limit", isRequired = false, type = INTEGER, defaultValue = "100",
180 description = "The maximum number of items to return per page."),
181 @RestParameter(name = "offset", isRequired = false, type = INTEGER, defaultValue = "0",
182 description = "The page number.")},
183 responses = {
184 @RestResponse(responseCode = SC_OK, description = "The groups.")})
185 public Response getGroups(@QueryParam("filter") String filter, @QueryParam("sort") String sort,
186 @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit) throws IOException {
187 Optional<Integer> optLimit = Optional.ofNullable(limit);
188 Optional<Integer> optOffset = Optional.ofNullable(offset);
189
190 Map<String, String> filters = RestUtils.parseFilter(filter);
191 Optional<String> optNameFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_NAME_NAME));
192 Optional<String> optRoleFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_ROLE_NAME));
193 Optional<String> optTextFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_TEXT_NAME));
194
195 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
196
197 List<JpaGroup> results = jpaGroupRoleProvider.getGroups(optLimit, optOffset, optNameFilter, optRoleFilter,
198 optTextFilter, sortCriteria);
199
200
201 List<String> userNames = results.stream().flatMap(item -> item.getMembers().stream())
202 .collect(Collectors.toList());
203 final Map<String, User> users = new HashMap<>(userNames.size());
204 userDirectoryService.loadUsers(userNames).forEachRemaining(user -> users.put(user.getUsername(), user));
205
206 List<JsonObject> groupsJsonArray = new ArrayList<>();
207 for (JpaGroup group : results) {
208 JsonObject groupJson = new JsonObject();
209 groupJson.addProperty("id", group.getGroupId());
210 groupJson.addProperty("name", safeString(group.getName()));
211 groupJson.addProperty("description", safeString(group.getDescription()));
212 groupJson.addProperty("role", group.getRole());
213 groupJson.add("users",
214 membersToJSON(group.getMembers().stream().map(users::get).filter(Objects::nonNull).iterator()));
215
216 groupsJsonArray.add(groupJson);
217 }
218
219 long dbTotal = jpaGroupRoleProvider.countTotalGroups(optNameFilter, optRoleFilter, optTextFilter);
220 long resultsTotal = optOffset.orElse(0) + results.size();
221
222
223 long total;
224
225 if (!optLimit.isPresent() || results.size() < optLimit.get()) {
226 total = resultsTotal;
227
228 } else {
229 total = max(dbTotal, resultsTotal);
230 }
231
232 return okJsonList(groupsJsonArray, optOffset, optLimit, total);
233 }
234
235 @DELETE
236 @Path("{id}")
237 @RestQuery(
238 name = "removegrouop",
239 description = "Remove a group",
240 returnDescription = "Returns no content",
241 pathParameters = {
242 @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING)},
243 responses = {
244 @RestResponse(responseCode = SC_OK, description = "Group deleted"),
245 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to delete the group with admin role."),
246 @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found."),
247 @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "An internal server error occured.")})
248 public Response removeGroup(@PathParam("id") String groupId) throws NotFoundException {
249 try {
250 jpaGroupRoleProvider.removeGroup(groupId);
251 return Response.noContent().build();
252 } catch (NotFoundException e) {
253 return Response.status(SC_NOT_FOUND).build();
254 } catch (UnauthorizedException e) {
255 return Response.status(SC_FORBIDDEN).build();
256 } catch (Exception e) {
257 logger.error("Unable to delete group {}", groupId, e);
258 throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
259 }
260 }
261
262 @POST
263 @Path("")
264 @RestQuery(
265 name = "createGroup",
266 description = "Add a group",
267 returnDescription = "Returns Created (201) if the group has been created",
268 restParameters = {
269 @RestParameter(name = "name", description = "The group name", isRequired = true, type = STRING),
270 @RestParameter(name = "description", description = "The group description", isRequired = false, type = STRING),
271 @RestParameter(name = "roles", description = "Comma seperated list of roles", isRequired = false, type = TEXT),
272 @RestParameter(name = "users", description = "Comma seperated list of members", isRequired = false, type = TEXT)},
273 responses = {
274 @RestResponse(responseCode = SC_CREATED, description = "Group created"),
275 @RestResponse(responseCode = SC_BAD_REQUEST, description = "Name too long"),
276 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to create a group with admin role."),
277 @RestResponse(responseCode = SC_CONFLICT, description = "An group with this name already exists.") })
278 public Response createGroup(@FormParam("name") String name, @FormParam("description") String description,
279 @FormParam("roles") String roles, @FormParam("users") String users) {
280 try {
281 jpaGroupRoleProvider.createGroup(name, description, roles, users);
282 } catch (IllegalArgumentException e) {
283 logger.warn("Unable to create group with name {}: {}", name, e.getMessage());
284 return Response.status(Status.BAD_REQUEST).build();
285 } catch (UnauthorizedException e) {
286 return Response.status(SC_FORBIDDEN).build();
287 } catch (ConflictException e) {
288 return Response.status(SC_CONFLICT).build();
289 }
290 return Response.status(Status.CREATED).build();
291 }
292
293 @PUT
294 @Path("{id}")
295 @RestQuery(
296 name = "updateGroup",
297 description = "Update a group",
298 returnDescription = "Return the status codes",
299 pathParameters = {
300 @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING) },
301 restParameters = {
302 @RestParameter(name = "name", description = "The group name", isRequired = true, type = STRING),
303 @RestParameter(name = "description", description = "The group description", isRequired = false, type = STRING),
304 @RestParameter(name = "roles", description = "Comma seperated list of roles", isRequired = false, type = TEXT),
305 @RestParameter(name = "users", description = "Comma seperated list of members", isRequired = false, type = TEXT)},
306 responses = {
307 @RestResponse(responseCode = SC_OK, description = "Group updated"),
308 @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to update the group with admin role."),
309 @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found"),
310 @RestResponse(responseCode = SC_BAD_REQUEST, description = "Name too long")})
311 public Response updateGroup(@PathParam("id") String groupId, @FormParam("name") String name,
312 @FormParam("description") String description, @FormParam("roles") String roles,
313 @FormParam("users") String users) throws NotFoundException {
314 try {
315 jpaGroupRoleProvider.updateGroup(groupId, name, description, roles, users);
316 } catch (IllegalArgumentException e) {
317 logger.warn("Unable to update group with id {}: {}", groupId, e.getMessage());
318 return Response.status(Status.BAD_REQUEST).build();
319 } catch (UnauthorizedException ex) {
320 return Response.status(SC_FORBIDDEN).build();
321 }
322 return Response.ok().build();
323 }
324
325 @GET
326 @Produces(MediaType.APPLICATION_JSON)
327 @Path("{id}")
328 @RestQuery(
329 name = "getGroup",
330 description = "Get a single group",
331 returnDescription = "Return the status codes",
332 pathParameters = {
333 @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING)},
334 responses = {
335 @RestResponse(responseCode = SC_OK, description = "Group found and returned as JSON"),
336 @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found")})
337 public Response getGroup(@PathParam("id") String groupId) throws NotFoundException {
338 JpaGroup group = jpaGroupRoleProvider.getGroup(groupId);
339
340 if (group == null) {
341 throw new NotFoundException("Group " + groupId + " does not exist.");
342 }
343
344
345 JsonArray rolesJSON = new JsonArray();
346 for (String role : group.getRoleNames()) {
347 rolesJSON.add(role);
348 }
349
350 Iterator<User> users = userDirectoryService.loadUsers(group.getMembers());
351 JsonObject jsonGroup = new JsonObject();
352 jsonGroup.addProperty("id", group.getGroupId());
353 jsonGroup.addProperty("name", safeString(group.getName()));
354 jsonGroup.addProperty("description", safeString(group.getDescription()));
355 jsonGroup.addProperty("role", safeString(group.getRole()));
356 jsonGroup.add("roles", rolesJSON);
357 jsonGroup.add("users", membersToJSON(users));
358
359 return RestUtils.okJson(jsonGroup);
360 }
361
362
363
364
365
366
367
368
369
370 private JsonArray membersToJSON(Iterator<User> members) {
371 JsonArray membersJSON = new JsonArray();
372
373 while (members.hasNext()) {
374 User user = members.next();
375 String name = user.getUsername();
376
377 if (StringUtils.isNotBlank(user.getName())) {
378 name = user.getName();
379 }
380
381 JsonObject jsonUser = new JsonObject();
382 jsonUser.addProperty("username", user.getUsername());
383 jsonUser.addProperty("name", name);
384
385 membersJSON.add(jsonUser);
386 }
387
388 return membersJSON;
389 }
390 }