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.authorization.xacml.manager.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_INTERNAL_SERVER_ERROR;
27  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
28  import static javax.servlet.http.HttpServletResponse.SC_OK;
29  import static org.opencastproject.util.RestUtil.R.conflict;
30  import static org.opencastproject.util.RestUtil.R.noContent;
31  import static org.opencastproject.util.RestUtil.R.notFound;
32  import static org.opencastproject.util.RestUtil.R.ok;
33  import static org.opencastproject.util.RestUtil.R.serverError;
34  import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
35  import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
36  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
37  
38  import org.opencastproject.assetmanager.api.AssetManager;
39  import org.opencastproject.authorization.xacml.manager.api.AclService;
40  import org.opencastproject.authorization.xacml.manager.api.AclServiceException;
41  import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory;
42  import org.opencastproject.authorization.xacml.manager.api.ManagedAcl;
43  import org.opencastproject.authorization.xacml.manager.impl.ManagedAclImpl;
44  import org.opencastproject.mediapackage.MediaPackage;
45  import org.opencastproject.mediapackage.MediaPackageException;
46  import org.opencastproject.security.api.AccessControlList;
47  import org.opencastproject.security.api.AccessControlParser;
48  import org.opencastproject.security.api.AccessControlUtil;
49  import org.opencastproject.security.api.AclScope;
50  import org.opencastproject.security.api.AuthorizationService;
51  import org.opencastproject.security.api.Organization;
52  import org.opencastproject.security.api.SecurityService;
53  import org.opencastproject.util.Jsons;
54  import org.opencastproject.util.NotFoundException;
55  import org.opencastproject.util.doc.rest.RestParameter;
56  import org.opencastproject.util.doc.rest.RestQuery;
57  import org.opencastproject.util.doc.rest.RestResponse;
58  
59  import org.apache.commons.lang3.StringUtils;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import java.util.List;
64  import java.util.Optional;
65  
66  import javax.ws.rs.DELETE;
67  import javax.ws.rs.FormParam;
68  import javax.ws.rs.GET;
69  import javax.ws.rs.POST;
70  import javax.ws.rs.PUT;
71  import javax.ws.rs.Path;
72  import javax.ws.rs.PathParam;
73  import javax.ws.rs.Produces;
74  import javax.ws.rs.WebApplicationException;
75  import javax.ws.rs.core.MediaType;
76  import javax.ws.rs.core.Response;
77  
78  public abstract class AbstractAclServiceRestEndpoint {
79    private static final Logger logger = LoggerFactory.getLogger(AbstractAclServiceRestEndpoint.class);
80  
81    protected abstract AclServiceFactory getAclServiceFactory();
82  
83    protected abstract SecurityService getSecurityService();
84  
85    protected abstract AssetManager getAssetManager();
86  
87    protected abstract AuthorizationService getAuthorizationService();
88  
89    @GET
90    @Path("/acl/{aclId}")
91    @Produces(MediaType.APPLICATION_JSON)
92    @RestQuery(
93        name = "getacl",
94        description = "Return the ACL by the given id",
95        returnDescription = "Return the ACL by the given id",
96        pathParameters = {
97            @RestParameter(name = "aclId", isRequired = true, description = "The ACL identifier", type = INTEGER)
98        },
99        responses = {
100           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"),
101           @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"),
102           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during returning the ACL")
103       }
104   )
105   public String getAcl(@PathParam("aclId") long aclId) throws NotFoundException {
106     final Optional<ManagedAcl> managedAcl = aclService().getAcl(aclId);
107     if (managedAcl.isEmpty()) {
108       logger.info("No ACL with id '{}' could be found", aclId);
109       throw new NotFoundException();
110     }
111     return JsonConv.full(managedAcl.get()).toJson();
112   }
113 
114   @POST
115   @Path("/acl/extend")
116   @Produces(MediaType.APPLICATION_JSON)
117   @RestQuery(
118       name = "extendacl",
119       description = "Return the given ACL with a new role and action in JSON format",
120       returnDescription = "Return the ACL with the new role and action in JSON format",
121       restParameters = {
122           @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING),
123           @RestParameter(name = "action", isRequired = true, description = "The action for the ACL", type = STRING),
124           @RestParameter(name = "role", isRequired = true, description = "The role for the ACL", type = STRING),
125           @RestParameter(
126               name = "allow",
127               isRequired = true,
128               description = "The allow status for the ACL",
129               type = BOOLEAN
130           )
131       },
132       responses = {
133           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"),
134           @RestResponse(responseCode = SC_BAD_REQUEST, description = "The ACL, action or role was invalid or empty"),
135           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during returning the ACL")
136       }
137   )
138   public String extendAcl(
139       @FormParam("acl") String accessControlList,
140       @FormParam("action") String action,
141       @FormParam("role") String role,
142       @FormParam("allow") boolean allow
143   ) {
144     if (StringUtils.isBlank(accessControlList) || StringUtils.isBlank(action) || StringUtils.isBlank(role)) {
145       throw new WebApplicationException(Response.Status.BAD_REQUEST);
146     }
147 
148     AccessControlList acl = AccessControlUtil.extendAcl(parseAcl(accessControlList), role, action, allow);
149     return JsonConv.full(acl).toJson();
150   }
151 
152   @POST
153   @Path("/acl/reduce")
154   @Produces(MediaType.APPLICATION_JSON)
155   @RestQuery(
156       name = "reduceacl",
157       description = "Return the given ACL without a role and action in JSON format",
158       returnDescription = "Return the ACL without the role and action in JSON format", restParameters = {
159           @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING),
160           @RestParameter(name = "action", isRequired = true, description = "The action for the ACL", type = STRING),
161           @RestParameter(name = "role", isRequired = true, description = "The role for the ACL", type = STRING)
162       },
163       responses = {
164           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"),
165           @RestResponse(responseCode = SC_BAD_REQUEST, description = "The ACL, role or action was invalid or empty"),
166           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during returning the ACL")
167       }
168   )
169   public String reduceAcl(
170       @FormParam("acl") String accessControlList,
171       @FormParam("action") String action,
172       @FormParam("role") String role
173   ) {
174     if (StringUtils.isBlank(accessControlList) || StringUtils.isBlank(action) || StringUtils.isBlank(role)) {
175       throw new WebApplicationException(Response.Status.BAD_REQUEST);
176     }
177 
178     AccessControlList acl = AccessControlUtil.reduceAcl(parseAcl(accessControlList), role, action);
179     return JsonConv.full(acl).toJson();
180   }
181 
182   @GET
183   @Path("/acl/acls.json")
184   @Produces(MediaType.APPLICATION_JSON)
185   @RestQuery(
186       name = "getacls",
187       description = "Lists the ACL's as JSON",
188       returnDescription = "The list of ACL's as JSON",
189       responses = {
190           @RestResponse(responseCode = SC_OK, description = "The list of ACL's has successfully been returned"),
191           @RestResponse(
192               responseCode = SC_INTERNAL_SERVER_ERROR,
193               description = "Error during returning the list of ACL's"
194           )
195       }
196   )
197   public String getAcls() {
198     List<Jsons.Val> acls = aclService().getAcls().stream()
199         .map(JsonConv.fullManagedAcl)
200         .toList();
201     return Jsons.arr(acls).toJson();
202   }
203 
204   @POST
205   @Path("/acl")
206   @Produces(MediaType.APPLICATION_JSON)
207   @RestQuery(
208       name = "createacl",
209       description = "Create an ACL",
210       returnDescription = "Create an ACL",
211       restParameters = {
212           @RestParameter(name = "name", isRequired = true, description = "The ACL name", type = STRING),
213           @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING)
214       },
215       responses = {
216           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been added"),
217           @RestResponse(responseCode = SC_CONFLICT, description = "An ACL with the same name already exists"),
218           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the ACL"),
219           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during adding the ACL")
220       }
221   )
222   public String createAcl(
223       @FormParam("name") String name,
224       @FormParam("acl") String accessControlList
225   ) {
226     final AccessControlList acl = parseAcl(accessControlList);
227     final Optional<ManagedAcl> managedAcl = aclService().createAcl(acl, name);
228     if (managedAcl.isEmpty()) {
229       logger.info("An ACL with the same name '{}' already exists", name);
230       throw new WebApplicationException(Response.Status.CONFLICT);
231     }
232     return JsonConv.full(managedAcl.get()).toJson();
233   }
234 
235   @PUT
236   @Path("/acl/{aclId}")
237   @Produces(MediaType.APPLICATION_JSON)
238   @RestQuery(
239       name = "updateacl",
240       description = "Update an ACL",
241       returnDescription = "Update an ACL",
242       pathParameters = {
243           @RestParameter(name = "aclId", isRequired = true, description = "The ACL identifier", type = INTEGER)
244       },
245       restParameters = {
246           @RestParameter(name = "name", isRequired = true, description = "The ACL name", type = STRING),
247           @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING)
248       },
249       responses = {
250           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been updated"),
251           @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"),
252           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the ACL"),
253           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during updating the ACL")
254       }
255   )
256   public String updateAcl(
257       @PathParam("aclId") long aclId,
258       @FormParam("name") String name,
259       @FormParam("acl") String accessControlList
260   ) throws NotFoundException {
261     final Organization org = getSecurityService().getOrganization();
262     final AccessControlList acl = parseAcl(accessControlList);
263     final ManagedAclImpl managedAcl = new ManagedAclImpl(aclId, name, org.getId(), acl);
264     if (!aclService().updateAcl(managedAcl)) {
265       logger.info("No ACL with id '{}' could be found under organization '{}'", aclId, org.getId());
266       throw new NotFoundException();
267     }
268     return JsonConv.full(managedAcl).toJson();
269   }
270 
271   @DELETE
272   @Path("/acl/{aclId}")
273   @RestQuery(
274       name = "deleteacl",
275       description = "Delete an ACL",
276       returnDescription = "Delete an ACL",
277       pathParameters = {
278           @RestParameter(name = "aclId", isRequired = true, description = "The ACL identifier", type = INTEGER)
279       },
280       responses = {
281           @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been deleted"),
282           @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"),
283           @RestResponse(
284               responseCode = SC_CONFLICT,
285               description = "The ACL could not be deleted, there are still references on it"
286           ),
287           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during deleting the ACL")
288       }
289   )
290   public Response deleteAcl(@PathParam("aclId") long aclId) throws NotFoundException {
291     try {
292       if (!aclService().deleteAcl(aclId)) {
293         return conflict();
294       }
295     } catch (AclServiceException e) {
296       logger.warn("Error deleting manged acl with id '{}'", aclId, e);
297       throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
298     }
299     return noContent();
300   }
301 
302   @POST
303   @Path("/apply/episode/{episodeId}")
304   @RestQuery(
305           name = "applyAclToEpisode",
306           description = "Immediate application of an ACL to an episode (Attention: This endpoint is deprecated and "
307                   + " will be removed in future versions!)",
308           returnDescription = "Status code",
309           pathParameters = {
310                   @RestParameter(name = "episodeId", isRequired = true, description = "The episode ID", type = STRING)
311           },
312           restParameters = {
313                   @RestParameter(
314                           name = "aclId",
315                           isRequired = false,
316                           description = "The ID of the ACL to apply. If missing the episode ACL will be "
317                                   + "deleted to fall back to the series ACL",
318                           type = INTEGER
319                   )
320           },
321           responses = {
322                   @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"),
323                   @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL or the episode has not been found"),
324                   @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error")
325           }
326   )
327   public Response applyAclToEpisode(@PathParam("episodeId") String episodeId, @FormParam("aclId") Long aclId) {
328     final AclService aclService = aclService();
329     Optional<ManagedAcl> macl = aclService.getAcl(aclId);
330     if (macl.isEmpty()) {
331       return notFound();
332     }
333     try {
334       Optional<AccessControlList> aclOpt = Optional.of(macl.get().getAcl());
335       Optional<MediaPackage> mediaPackage = getAssetManager().getMediaPackage(episodeId);
336       // the episode service is the source of authority for the retrieval of media packages
337       if (mediaPackage.isPresent()) {
338         MediaPackage episodeSvcMp = mediaPackage.get();
339         aclOpt.ifPresentOrElse(
340             acl -> { // "some" branch
341               try {
342                 MediaPackage mp = getAuthorizationService()
343                     .setAcl(episodeSvcMp, AclScope.Episode, acl)
344                     .getA();
345                 getAssetManager().takeSnapshot(mp);
346               } catch (MediaPackageException e) {
347                 logger.error("Error getting ACL from media package", e);
348               }
349             },
350             () -> { // "none" branch
351               MediaPackage mp = getAuthorizationService()
352                   .removeAcl(episodeSvcMp, AclScope.Episode);
353               getAssetManager().takeSnapshot(mp);
354             }
355         );
356         return ok();
357       }
358       // not found
359       return notFound();
360     } catch (Exception e) {
361       logger.error("Error applying acl to episode {}", episodeId);
362       return serverError();
363     }
364   }
365 
366   private static AccessControlList parseAcl(String acl) {
367     try {
368       return AccessControlParser.parseAcl(acl);
369     } catch (Exception e) {
370       logger.warn("Unable to parse ACL");
371       throw new WebApplicationException(Response.Status.BAD_REQUEST);
372     }
373   }
374 
375   private AclService aclService() {
376     return getAclServiceFactory().serviceFor(getSecurityService().getOrganization());
377   }
378 }