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