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 javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25  import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
26  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
27  import static javax.servlet.http.HttpServletResponse.SC_OK;
28  import static org.apache.commons.lang3.StringUtils.trimToNull;
29  import static org.opencastproject.index.service.util.RestUtils.okJsonList;
30  import static org.opencastproject.userdirectory.UserIdRoleProvider.getUserRolePrefix;
31  import static org.opencastproject.userdirectory.UserIdRoleProvider.isSanitize;
32  import static org.opencastproject.util.RestUtil.R.conflict;
33  import static org.opencastproject.util.RestUtil.R.noContent;
34  import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
35  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
36  
37  import org.opencastproject.adminui.util.TextFilter;
38  import org.opencastproject.authorization.xacml.manager.api.AclService;
39  import org.opencastproject.authorization.xacml.manager.api.AclServiceException;
40  import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory;
41  import org.opencastproject.authorization.xacml.manager.api.ManagedAcl;
42  import org.opencastproject.authorization.xacml.manager.endpoint.JsonConv;
43  import org.opencastproject.authorization.xacml.manager.impl.ManagedAclImpl;
44  import org.opencastproject.index.service.resources.list.query.AclsListQuery;
45  import org.opencastproject.index.service.util.RestUtils;
46  import org.opencastproject.security.api.AccessControlEntry;
47  import org.opencastproject.security.api.AccessControlList;
48  import org.opencastproject.security.api.AccessControlParser;
49  import org.opencastproject.security.api.Organization;
50  import org.opencastproject.security.api.Role;
51  import org.opencastproject.security.api.RoleDirectoryService;
52  import org.opencastproject.security.api.SecurityService;
53  import org.opencastproject.security.api.User;
54  import org.opencastproject.security.api.UserDirectoryService;
55  import org.opencastproject.util.NotFoundException;
56  import org.opencastproject.util.doc.rest.RestParameter;
57  import org.opencastproject.util.doc.rest.RestQuery;
58  import org.opencastproject.util.doc.rest.RestResponse;
59  import org.opencastproject.util.doc.rest.RestService;
60  import org.opencastproject.util.requests.SortCriterion;
61  import org.opencastproject.util.requests.SortCriterion.Order;
62  
63  import com.google.gson.JsonArray;
64  import com.google.gson.JsonObject;
65  
66  import org.apache.commons.lang3.ObjectUtils;
67  import org.apache.commons.lang3.StringUtils;
68  import org.json.simple.JSONArray;
69  import org.json.simple.JSONObject;
70  import org.osgi.service.component.ComponentContext;
71  import org.osgi.service.component.annotations.Activate;
72  import org.osgi.service.component.annotations.Component;
73  import org.osgi.service.component.annotations.Reference;
74  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
75  import org.slf4j.Logger;
76  import org.slf4j.LoggerFactory;
77  
78  import java.io.IOException;
79  import java.util.ArrayList;
80  import java.util.Collections;
81  import java.util.Comparator;
82  import java.util.HashMap;
83  import java.util.LinkedHashSet;
84  import java.util.List;
85  import java.util.Map;
86  import java.util.Optional;
87  import java.util.Set;
88  
89  import javax.ws.rs.DELETE;
90  import javax.ws.rs.FormParam;
91  import javax.ws.rs.GET;
92  import javax.ws.rs.POST;
93  import javax.ws.rs.PUT;
94  import javax.ws.rs.Path;
95  import javax.ws.rs.PathParam;
96  import javax.ws.rs.Produces;
97  import javax.ws.rs.QueryParam;
98  import javax.ws.rs.WebApplicationException;
99  import javax.ws.rs.core.MediaType;
100 import javax.ws.rs.core.Response;
101 
102 @Path("/admin-ng/acl")
103 @RestService(
104     name = "acl",
105     title = "Acl service",
106     abstractText = "Provides operations for acl",
107     notes = { "This service offers the default acl CRUD Operations for the admin UI.",
108               "<strong>Important:</strong> "
109                 + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
110                 + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
111                 + "DO NOT use this for integration of third-party applications.<em>"})
112 @Component(
113     immediate = true,
114     service = AclEndpoint.class,
115     property = {
116         "service.description=Admin UI - ACL Endpoint",
117         "opencast.service.type=org.opencastproject.adminui.AclEndpoint",
118         "opencast.service.path=/admin-ng/acl",
119     }
120 )
121 @JaxrsResource
122 public class AclEndpoint {
123 
124   /** The logging facility */
125   private static final Logger logger = LoggerFactory.getLogger(AclEndpoint.class);
126 
127   /** The acl service factory */
128   private AclServiceFactory aclServiceFactory;
129 
130   /** The security service */
131   private SecurityService securityService;
132 
133   // The role directory service
134   private RoleDirectoryService roleDirectoryService;
135 
136   /** The global user directory service */
137   protected UserDirectoryService userDirectoryService;
138 
139   /**
140    * @param aclServiceFactory
141    *          the aclServiceFactory to set
142    */
143   @Reference
144   public void setAclServiceFactory(AclServiceFactory aclServiceFactory) {
145     this.aclServiceFactory = aclServiceFactory;
146   }
147 
148   /** OSGi callback for role directory service. */
149   @Reference
150   public void setRoleDirectoryService(RoleDirectoryService roleDirectoryService) {
151     this.roleDirectoryService = roleDirectoryService;
152   }
153 
154   /**
155    * @param securityService
156    *          the securityService to set
157    */
158   @Reference
159   public void setSecurityService(SecurityService securityService) {
160     this.securityService = securityService;
161   }
162 
163   /** OSGi callback. */
164   @Activate
165   protected void activate(ComponentContext cc) {
166     logger.info("Activate the Admin ui - Acl facade endpoint");
167   }
168 
169   private AclService aclService() {
170     return aclServiceFactory.serviceFor(securityService.getOrganization());
171   }
172 
173   @Reference
174   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
175     this.userDirectoryService = userDirectoryService;
176   }
177 
178   @GET
179   @Path("acls.json")
180   @Produces(MediaType.APPLICATION_JSON)
181   @RestQuery(
182       name = "allaclasjson",
183       description = "Returns a list of acls",
184       returnDescription = "Returns a JSON representation of the list of acls available the current user's organization",
185       restParameters = {
186           @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They "
187               + "should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
188           @RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any of the "
189               + "following: NAME. Add '_DESC' to reverse the sort order (e.g. NAME_DESC).", type = STRING),
190           @RestParameter(defaultValue = "100", description = "The maximum number of items to return per page.",
191               isRequired = false, name = "limit", type = RestParameter.Type.STRING),
192           @RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset",
193               type = RestParameter.Type.STRING)
194       },
195       responses = {
196           @RestResponse(responseCode = SC_OK, description = "The list of ACL's has successfully been returned")
197       })
198   public Response getAclsAsJson(@QueryParam("filter") String filter, @QueryParam("sort") String sort,
199           @QueryParam("offset") int offset, @QueryParam("limit") int limit) throws IOException {
200     if (limit < 1) {
201       limit = 100;
202     }
203     Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
204     Optional<String> filterName = Optional.empty();
205     Optional<String> filterText = Optional.empty();
206 
207     Map<String, String> filters = RestUtils.parseFilter(filter);
208     for (String name : filters.keySet()) {
209       String value = filters.get(name);
210       if (AclsListQuery.FILTER_NAME_NAME.equals(name)) {
211         filterName = Optional.of(value);
212       } else if ((AclsListQuery.FILTER_TEXT_NAME.equals(name)) && (StringUtils.isNotBlank(value))) {
213         filterText = Optional.of(value);
214       }
215     }
216 
217     // Filter acls by filter criteria
218     List<ManagedAcl> filteredAcls = new ArrayList<>();
219     for (ManagedAcl acl : aclService().getAcls()) {
220       // Filter list
221       if ((filterName.isPresent() && !filterName.get().equals(acl.getName()))
222               || (filterText.isPresent() && !TextFilter.match(filterText.get(), acl.getName()))) {
223         continue;
224       }
225       filteredAcls.add(acl);
226     }
227     int total = filteredAcls.size();
228 
229     // Sort by name, description or role
230     if (optSort.isPresent()) {
231       final ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
232       Collections.sort(filteredAcls, new Comparator<ManagedAcl>() {
233         @Override
234         public int compare(ManagedAcl acl1, ManagedAcl acl2) {
235           for (SortCriterion criterion : sortCriteria) {
236             Order order = criterion.getOrder();
237             switch (criterion.getFieldName()) {
238               case "name":
239                 if (order.equals(Order.Descending)) {
240                   return ObjectUtils.compare(acl2.getName(), acl1.getName());
241                 }
242                 return ObjectUtils.compare(acl1.getName(), acl2.getName());
243               default:
244                 logger.info("Unkown sort type: {}", criterion.getFieldName());
245                 return 0;
246             }
247           }
248           return 0;
249         }
250       });
251     }
252 
253     int start = Math.min(offset, filteredAcls.size());
254     int end = Math.min(start + limit, filteredAcls.size());
255 
256     // Apply Limit and offset
257     List<ManagedAcl> subList = filteredAcls.subList(start, end);
258 
259     // Convert each ManagedAcl to JsonObject using a helper method
260     List<JsonObject> aclJSON = new ArrayList<>();
261     for (ManagedAcl acl : subList) {
262       aclJSON.add(full(acl));
263     }
264 
265     return okJsonList(aclJSON, offset, limit, total);
266   }
267 
268   @GET
269   @Path("roles.json")
270   @Produces(MediaType.APPLICATION_JSON)
271   @RestQuery(
272       name = "getRoles",
273       description = "Returns a list of roles",
274       returnDescription = "Returns a JSON representation of the roles with the given parameters under the "
275           + "current user's organization.",
276       restParameters = {
277           @RestParameter(name = "query", isRequired = false, description = "The query.", type = STRING),
278           @RestParameter(name = "target", isRequired = false, description = "The target of the roles.",
279               type = STRING),
280           @RestParameter(name = "limit", defaultValue = "100",
281               description = "The maximum number of items to return per page.", isRequired = false,
282               type = RestParameter.Type.STRING),
283           @RestParameter(name = "offset", defaultValue = "0", description = "The page number.", isRequired = false,
284               type = RestParameter.Type.STRING)
285       },
286       responses = {
287           @RestResponse(responseCode = SC_OK, description = "The list of roles.")
288       })
289   public Response getRoles(@QueryParam("query") String query, @QueryParam("target") String target,
290       @QueryParam("offset") int offset, @QueryParam("limit") int limit) {
291 
292     String roleQuery = "%";
293     if (StringUtils.isNotBlank(query)) {
294       roleQuery = query.trim() + "%";
295     }
296 
297     Role.Target roleTarget = Role.Target.ALL;
298 
299     if (StringUtils.isNotBlank(target)) {
300       try {
301         roleTarget = Role.Target.valueOf(target.trim());
302       } catch (Exception e) {
303         logger.warn("Invalid target filter value {}", target);
304       }
305     }
306 
307     List<Role> roles = roleDirectoryService.findRoles(roleQuery, roleTarget, offset, limit);
308     Set<Role> uniqueRoles = new LinkedHashSet<>(roles);
309 
310     JSONArray jsonRoles = new JSONArray();
311     for (Role role: uniqueRoles) {
312       JSONObject jsonRole = new JSONObject();
313       jsonRole.put("name", role.getName());
314       jsonRole.put("type", role.getType().toString());
315       jsonRole.put("description", role.getDescription());
316       jsonRole.put("organization", role.getOrganizationId());
317       jsonRole.put("isSanitize", isSanitize());
318       if (!isSanitize()) {
319         User user = userDirectoryService.loadUser(role.getName().replaceFirst(getUserRolePrefix(), ""));
320         if (user != null) {
321           jsonRole.put("user", generateJsonUser(user));
322         }
323       }
324       jsonRoles.add(jsonRole);
325     }
326 
327     return Response.ok(jsonRoles.toJSONString()).build();
328   }
329 
330   @DELETE
331   @Path("{id}")
332   @RestQuery(
333       name = "deleteacl",
334       description = "Delete an ACL",
335       returnDescription = "Delete an ACL",
336       pathParameters = {
337           @RestParameter(name = "id", isRequired = true, description = "The ACL identifier", type = INTEGER)
338       },
339       responses = {
340           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been deleted"),
341           @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"),
342           @RestResponse(responseCode = SC_CONFLICT, description = "The ACL could not be deleted, there are still "
343               + "references on it")
344       })
345   public Response deleteAcl(@PathParam("id") long aclId) throws NotFoundException {
346     try {
347       if (!aclService().deleteAcl(aclId)) {
348         return conflict();
349       }
350     } catch (AclServiceException e) {
351       logger.warn("Error deleting manged acl with id '{}'", aclId, e);
352       throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
353     }
354     return noContent();
355   }
356 
357   @POST
358   @Path("")
359   @Produces(MediaType.APPLICATION_JSON)
360   @RestQuery(
361       name = "createacl",
362       description = "Create an ACL",
363       returnDescription = "Create an ACL",
364       restParameters = {
365           @RestParameter(name = "name", isRequired = true, description = "The ACL name", type = STRING),
366           @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING)
367       },
368       responses = {
369           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been added"),
370           @RestResponse(responseCode = SC_CONFLICT, description = "An ACL with the same name already exists"),
371           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the ACL")
372       })
373   public Response createAcl(@FormParam("name") String name, @FormParam("acl") String accessControlList) {
374     final AccessControlList acl = parseAcl(accessControlList);
375     Optional<ManagedAcl> managedAcl = aclService().createAcl(acl, name);
376     if (managedAcl.isEmpty()) {
377       logger.info("An ACL with the same name '{}' already exists", name);
378       throw new WebApplicationException(Response.Status.CONFLICT);
379     }
380     return RestUtils.okJson(full(managedAcl.get()));
381   }
382 
383   @PUT
384   @Path("{id}")
385   @Produces(MediaType.APPLICATION_JSON)
386   @RestQuery(
387       name = "updateacl",
388       description = "Update an ACL",
389       returnDescription = "Update an ACL",
390       pathParameters = {
391           @RestParameter(name = "id", isRequired = true, description = "The ACL identifier", type = INTEGER)
392       },
393       restParameters = {
394           @RestParameter(name = "name", isRequired = true, description = "The ACL name", type = STRING),
395           @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING)
396       },
397       responses = {
398           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been updated"),
399           @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"),
400           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the ACL")
401       })
402   public Response updateAcl(@PathParam("id") long aclId, @FormParam("name") String name,
403       @FormParam("acl") String accessControlList) throws NotFoundException {
404     final Organization org = securityService.getOrganization();
405     final AccessControlList acl = parseAcl(accessControlList);
406     final ManagedAclImpl managedAcl = new ManagedAclImpl(aclId, name, org.getId(), acl);
407     if (!aclService().updateAcl(managedAcl)) {
408       logger.info("No ACL with id '{}' could be found under organization '{}'", aclId, org.getId());
409       throw new NotFoundException();
410     }
411     return RestUtils.okJson(full(managedAcl));
412   }
413 
414   @GET
415   @Path("{id}")
416   @Produces(MediaType.APPLICATION_JSON)
417   @RestQuery(
418       name = "getacl",
419       description = "Return the ACL by the given id",
420       returnDescription = "Return the ACL by the given id",
421       pathParameters = {
422           @RestParameter(name = "id", isRequired = true, description = "The ACL identifier", type = INTEGER)
423       },
424       responses = {
425           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"),
426           @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found")
427       })
428   public Response getAcl(@PathParam("id") long aclId) throws NotFoundException {
429     Optional<ManagedAcl> managedAcl = aclService().getAcl(aclId);
430     if (managedAcl.isPresent()) {
431       return RestUtils.okJson(full(managedAcl.get()));
432     }
433     logger.info("No ACL with id '{}' could by found", aclId);
434     throw new NotFoundException();
435   }
436 
437   @GET
438   @Path("acl/{name}")
439   @Produces(MediaType.APPLICATION_JSON)
440   @RestQuery(
441       name = "getaclbyname",
442       description = "Return the ACL by the given name",
443       returnDescription = "Return the ACL by the given name",
444       pathParameters = {
445           @RestParameter(name = "name", isRequired = true, description = "The ACL name", type = STRING)
446       },
447       responses = {
448           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"),
449           @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found")
450       })
451   public Response getAcl(@PathParam("name") String aclName) throws NotFoundException {
452     Optional<ManagedAcl> managedAcl = aclService().getAcl(aclName);
453     if (managedAcl.isPresent()) {
454       return RestUtils.okJson(full(managedAcl.get()));
455     }
456     logger.info("No ACL with name '{}' could by found", aclName);
457     throw new NotFoundException();
458   }
459 
460   private static AccessControlList parseAcl(String acl) {
461     try {
462       return AccessControlParser.parseAcl(acl);
463     } catch (Exception e) {
464       logger.warn("Unable to parse ACL", e);
465       throw new WebApplicationException(Response.Status.BAD_REQUEST);
466     }
467   }
468 
469   public JsonObject full(AccessControlEntry ace) {
470     JsonObject json = new JsonObject();
471     json.addProperty(JsonConv.KEY_ROLE, ace.getRole());
472     json.addProperty(JsonConv.KEY_ACTION, ace.getAction());
473     json.addProperty(JsonConv.KEY_ALLOW, ace.isAllow());
474     return json;
475   }
476 
477   private JsonObject fullAccessControlEntry(AccessControlEntry ace) {
478     return full(ace);
479   }
480 
481   public JsonObject full(AccessControlList acl) {
482     JsonObject json = new JsonObject();
483     JsonArray aceArray = new JsonArray();
484 
485     List<AccessControlEntry> entries = acl.getEntries();
486     if (entries != null) {
487       for (AccessControlEntry entry : entries) {
488         aceArray.add(fullAccessControlEntry(entry));
489       }
490     }
491 
492     json.add(JsonConv.KEY_ACE, aceArray);
493     return json;
494   }
495 
496   public JsonObject full(ManagedAcl acl) {
497     JsonObject json = new JsonObject();
498     json.addProperty(JsonConv.KEY_ID, acl.getId());
499     json.addProperty(JsonConv.KEY_NAME, acl.getName());
500     json.addProperty(JsonConv.KEY_ORGANIZATION_ID, acl.getOrganizationId());
501     json.add("acl", full(acl.getAcl()));
502     return json;
503   }
504 
505   public Map<String, Object> generateJsonUser(User user) {
506     // Prepare the roles
507     Map<String, Object> userData = new HashMap<>();
508     userData.put("username", user.getUsername());
509     userData.put("name", user.getName());
510     userData.put("email", user.getEmail());
511     return userData;
512   }
513 
514 }