/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.authorization.xacml.manager.endpoint; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.opencastproject.assetmanager.api.fn.Enrichments.enrich; import static org.opencastproject.authorization.xacml.manager.endpoint.JsonConv.digestManagedAcl; import static org.opencastproject.authorization.xacml.manager.endpoint.JsonConv.fullAccessControlList; import static org.opencastproject.authorization.xacml.manager.endpoint.JsonConv.nest; import static org.opencastproject.authorization.xacml.manager.impl.Util.getManagedAcl; import static org.opencastproject.security.api.AccessControlUtil.acl; import static org.opencastproject.util.Jsons.arr; import static org.opencastproject.util.Jsons.obj; import static org.opencastproject.util.Jsons.p; import static org.opencastproject.util.RestUtil.R.badRequest; import static org.opencastproject.util.RestUtil.R.conflict; import static org.opencastproject.util.RestUtil.R.noContent; import static org.opencastproject.util.RestUtil.R.notFound; import static org.opencastproject.util.RestUtil.R.ok; import static org.opencastproject.util.RestUtil.R.serverError; import static org.opencastproject.util.RestUtil.splitCommaSeparatedParam; import static org.opencastproject.util.data.Either.left; import static org.opencastproject.util.data.Either.right; import static org.opencastproject.util.data.Monadics.mlist; import static org.opencastproject.util.data.Option.option; import static org.opencastproject.util.data.Prelude.unexhaustiveMatch; import static org.opencastproject.util.data.Tuple.tuple; import static org.opencastproject.util.data.functions.Misc.chuck; import static org.opencastproject.util.data.functions.Strings.trimToNone; import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN; import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER; import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING; import org.opencastproject.assetmanager.api.AssetManager; import org.opencastproject.assetmanager.api.Snapshot; import org.opencastproject.assetmanager.api.query.AQueryBuilder; import org.opencastproject.assetmanager.api.query.ASelectQuery; import org.opencastproject.authorization.xacml.manager.api.AclService; import org.opencastproject.authorization.xacml.manager.api.AclServiceException; import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory; import org.opencastproject.authorization.xacml.manager.api.AclServiceNoReferenceException; import org.opencastproject.authorization.xacml.manager.api.EpisodeACLTransition; import org.opencastproject.authorization.xacml.manager.api.ManagedAcl; import org.opencastproject.authorization.xacml.manager.api.SeriesACLTransition; import org.opencastproject.authorization.xacml.manager.api.TransitionQuery; import org.opencastproject.authorization.xacml.manager.api.TransitionResult; import org.opencastproject.authorization.xacml.manager.impl.AclTransitionDbDuplicatedException; import org.opencastproject.authorization.xacml.manager.impl.ManagedAclImpl; import org.opencastproject.authorization.xacml.manager.impl.Util; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.security.api.AccessControlParser; import org.opencastproject.security.api.AccessControlUtil; import org.opencastproject.security.api.AclScope; import org.opencastproject.security.api.AuthorizationService; import org.opencastproject.security.api.Organization; import org.opencastproject.security.api.SecurityService; import org.opencastproject.series.api.SeriesException; import org.opencastproject.series.api.SeriesService; import org.opencastproject.util.DateTimeSupport; import org.opencastproject.util.Jsons; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.data.Either; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Function2; import org.opencastproject.util.data.Monadics; import org.opencastproject.util.data.MultiMap; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.Predicate; import org.opencastproject.util.data.Tuple; import org.opencastproject.util.data.functions.Functions; import org.opencastproject.util.data.functions.Options; import org.opencastproject.util.doc.rest.RestParameter; import org.opencastproject.util.doc.rest.RestQuery; import org.opencastproject.util.doc.rest.RestResponse; import org.opencastproject.workflow.api.ConfiguredWorkflowRef; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.List; import java.util.Map; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; public abstract class AbstractAclServiceRestEndpoint { private static final Logger logger = LoggerFactory.getLogger(AbstractAclServiceRestEndpoint.class); private static final AccessControlList EMPTY_ACL = acl(); protected abstract AclServiceFactory getAclServiceFactory(); protected abstract String getEndpointBaseUrl(); protected abstract SecurityService getSecurityService(); protected abstract AuthorizationService getAuthorizationService(); protected abstract AssetManager getAssetManager(); protected abstract SeriesService getSeriesService(); @PUT @Path("/series/{transitionId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "updateseriestransition", description = "Update an existing series transition", returnDescription = "Update an existing series transition", pathParameters = { @RestParameter(name = "transitionId", isRequired = true, description = "The transition id", type = STRING) }, restParameters = { @RestParameter(name = "applicationDate", isRequired = true, description = "The date to applicate", type = STRING), @RestParameter(name = "managedAclId", isRequired = true, description = "The managed access control list id", type = INTEGER), @RestParameter(name = "workflowDefinitionId", isRequired = false, description = "The workflow definition identifier", type = STRING), @RestParameter(name = "workflowParams", isRequired = false, description = "The workflow parameters as JSON", type = STRING), @RestParameter(name = "override", isRequired = false, description = "If to override the episode ACL's", type = STRING, defaultValue = "false") }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series transition has successfully been updated"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "The given managed acl id could not be found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during updating a series transition") }) public String updateSeriesTransition(@PathParam("transitionId") long transitionId, @FormParam("applicationDate") String applicationDate, @FormParam("managedAclId") long managedAclId, @FormParam("workflowDefinitionId") String workflowDefinitionId, @FormParam("workflowParams") String workflowParams, @FormParam("override") boolean override) throws NotFoundException { try { final Date at = new Date(DateTimeSupport.fromUTC(applicationDate)); final Option<ConfiguredWorkflowRef> workflow = createConfiguredWorkflowRef(workflowDefinitionId, workflowParams); final SeriesACLTransition t = aclService().updateSeriesTransition(transitionId, managedAclId, at, workflow, override); return JsonConv.full(t).toJson(); } catch (AclServiceNoReferenceException e) { logger.info("Managed acl with id '{}' could not be found", managedAclId); throw new WebApplicationException(Status.BAD_REQUEST); } catch (AclServiceException e) { logger.warn("Error updating series transition: {}", e); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.warn("Unable to parse the application date"); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } @PUT @Path("/episode/{transitionId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "updateepisodetransition", description = "Update an existing episode transition", returnDescription = "Update an existing episode transition", pathParameters = { @RestParameter(name = "transitionId", isRequired = true, description = "The transition id", type = STRING) }, restParameters = { @RestParameter(name = "applicationDate", isRequired = true, description = "The date to applicate", type = STRING), @RestParameter(name = "managedAclId", isRequired = false, description = "The managed access control list id", type = INTEGER), @RestParameter(name = "workflowDefinitionId", isRequired = false, description = "The workflow definition identifier", type = STRING), @RestParameter(name = "workflowParams", isRequired = false, description = "The workflow parameters as JSON", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The episode transition has successfully been updated"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during updating an episode transition") }) public String updateEpisodeTransition(@PathParam("transitionId") long transitionId, @FormParam("applicationDate") String applicationDate, @FormParam("managedAclId") Long managedAclId, @FormParam("workflowDefinitionId") String workflowDefinitionId, @FormParam("workflowParams") String workflowParams) throws NotFoundException { try { final Date at = new Date(DateTimeSupport.fromUTC(applicationDate)); final Option<ConfiguredWorkflowRef> workflow = createConfiguredWorkflowRef(workflowDefinitionId, workflowParams); final EpisodeACLTransition t = aclService().updateEpisodeTransition(transitionId, option(managedAclId), at, workflow); return JsonConv.full(t).toJson(); } catch (AclServiceException e) { logger.warn("Error updating episode transition: {}", e); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.warn("Unable to parse the application date"); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } @POST @Path("/series/{seriesId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "addseriestransition", description = "Add a series transition", returnDescription = "Add a series transition", pathParameters = { @RestParameter(name = "seriesId", isRequired = true, description = "The series id", type = STRING) }, restParameters = { @RestParameter(name = "applicationDate", isRequired = true, description = "The date to applicate", type = STRING), @RestParameter(name = "managedAclId", isRequired = true, description = "The managed access control list id", type = INTEGER), @RestParameter(name = "workflowDefinitionId", isRequired = false, description = "The workflow definition identifier", type = STRING), @RestParameter(name = "workflowParams", isRequired = false, description = "The workflow parameters as JSON", type = STRING), @RestParameter(name = "override", isRequired = false, description = "If to override the episode ACL's", type = STRING, defaultValue = "false") }, reponses = { @RestResponse(responseCode = SC_OK, description = "The series transition has successfully been added"), @RestResponse(responseCode = SC_CONFLICT, description = "The series transition with the applicationDate already exists"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "The given managed acl id could not be found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during adding a series transition") }) public String addSeriesTransition(@PathParam("seriesId") String seriesId, @FormParam("applicationDate") String applicationDate, @FormParam("managedAclId") long managedAclId, @FormParam("workflowDefinitionId") String workflowDefinitionId, @FormParam("workflowParams") String workflowParams, @FormParam("override") boolean override) { try { final Date at = new Date(DateTimeSupport.fromUTC(applicationDate)); final Option<ConfiguredWorkflowRef> workflow = createConfiguredWorkflowRef(workflowDefinitionId, workflowParams); SeriesACLTransition seriesTransition = aclService().addSeriesTransition(seriesId, managedAclId, at, override, workflow); return JsonConv.full(seriesTransition).toJson(); } catch (AclServiceNoReferenceException e) { logger.info("Managed acl with id '{}' coudl not be found", managedAclId); throw new WebApplicationException(Status.BAD_REQUEST); } catch (AclTransitionDbDuplicatedException e) { logger.info("Error adding series transition: transition with date {} already exists", applicationDate); throw new WebApplicationException(Status.CONFLICT); } catch (AclServiceException e) { logger.warn("Error adding series transition: {}", e); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } catch (Exception e) { logger.warn("Unable to parse the application date"); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } @POST @Path("/episode/{episodeId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "addepisodetransition", description = "Add an episode transition", returnDescription = "Add an episode transition", pathParameters = { @RestParameter(name = "episodeId", isRequired = true, description = "The episode id", type = STRING) }, restParameters = { @RestParameter(name = "applicationDate", isRequired = true, description = "The date to applicate", type = STRING), @RestParameter(name = "managedAclId", isRequired = false, description = "The managed access control list id", type = INTEGER), @RestParameter(name = "workflowDefinitionId", isRequired = false, description = "The workflow definition identifier", type = STRING), @RestParameter(name = "workflowParams", isRequired = false, description = "The workflow parameters as JSON", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The episode transition has successfully been added"), @RestResponse(responseCode = SC_CONFLICT, description = "The episode transition with the applicationDate already exists"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during adding an episode transition") }) public String addEpisodeTransition(@PathParam("episodeId") String episodeId, @FormParam("applicationDate") String applicationDate, @FormParam("managedAclId") Long managedAclId, @FormParam("workflowDefinitionId") String workflowDefinitionId, @FormParam("workflowParams") String workflowParams) { try { final Date at = new Date(DateTimeSupport.fromUTC(applicationDate)); final Option<ConfiguredWorkflowRef> workflow = createConfiguredWorkflowRef(workflowDefinitionId, workflowParams); final EpisodeACLTransition transition = aclService().addEpisodeTransition(episodeId, option(managedAclId), at, workflow); return JsonConv.full(transition).toJson(); } catch (AclTransitionDbDuplicatedException e) { logger.info("Error adding episode transition: transition with date {} already exists", applicationDate); throw new WebApplicationException(Status.CONFLICT); } catch (AclServiceException e) { logger.warn("Error adding episode transition: {}", e); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } catch (Exception e) { logger.warn("Unable to parse the application date"); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } @DELETE @Path("/episode/{transitionId}") @RestQuery(name = "deleteepisodetransition", description = "Delets an episode transition", returnDescription = "Delets an episode transition", pathParameters = { @RestParameter(name = "transitionId", isRequired = true, description = "The transition id", type = STRING) }, reponses = { @RestResponse(responseCode = SC_NO_CONTENT, description = "The episode transition has been deleted successfully"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The episode transition has not been found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during deleting the episode transition") }) public Response deleteEpisodeTransition(@PathParam("transitionId") long transitionId) throws NotFoundException { try { aclService().deleteEpisodeTransition(transitionId); return Response.noContent().build(); } catch (NotFoundException e) { throw e; } catch (AclServiceException e) { logger.warn("Error deleting episode transition {}: {}", transitionId, e); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } @DELETE @Path("/series/{transitionId}") @RestQuery(name = "deleteseriestransition", description = "Delets a series transition", returnDescription = "Delets a series transition", pathParameters = { @RestParameter(name = "transitionId", isRequired = true, description = "The transition id", type = STRING) }, reponses = { @RestResponse(responseCode = SC_NO_CONTENT, description = "The series transition has been deleted successfully"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The series transition has not been found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during deleting the series transition") }) public Response deleteSeriesTransition(@PathParam("transitionId") long transitionId) throws NotFoundException { try { aclService().deleteSeriesTransition(transitionId); return Response.noContent().build(); } catch (NotFoundException e) { throw e; } catch (AclServiceException e) { logger.warn("Error deleting series transition {}: {}", transitionId, e); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } } @GET @Path("/transitionsfor.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getTransitionsForAsJson", description = "Get the transitions for a list of episodes and/or series as json. At least one of the lists must not be empty.", returnDescription = "Get the transitions as json", restParameters = { @RestParameter(name = "episodeIds", isRequired = false, description = "A list of comma separated episode IDs", type = STRING), @RestParameter(name = "seriesIds", isRequired = false, description = "A list of comma separated series IDs", type = STRING), @RestParameter(name = "done", isRequired = false, description = "Indicates if already applied transitions should be included", type = BOOLEAN) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The request was processed succesfully"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "Parameter error"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during processing the request") }) public Response getTransitionsFor(@QueryParam("episodeIds") String episodeIds, @QueryParam("seriesIds") String seriesIds, @DefaultValue("false") @QueryParam("done") final boolean done) { final Monadics.ListMonadic<String> eIds = splitCommaSeparatedParam(option(episodeIds)); final Monadics.ListMonadic<String> sIds = splitCommaSeparatedParam(option(seriesIds)); if (eIds.value().isEmpty() && sIds.value().isEmpty()) { return badRequest(); } final AclService aclService = aclService(); try { // episodeId -> [transitions] final Map<String, List<EpisodeACLTransition>> eTs = eIds .foldl(MultiMap.<String, EpisodeACLTransition> multiHashMapWithArrayList(), new Function2.X<MultiMap<String, EpisodeACLTransition>, String, MultiMap<String, EpisodeACLTransition>>() { @Override public MultiMap<String, EpisodeACLTransition> xapply( MultiMap<String, EpisodeACLTransition> mmap, String id) throws Exception { // todo it is quite expensive to query each episode separately final TransitionQuery q = TransitionQuery.query().withId(id).withScope(AclScope.Episode) .withDone(done); return mmap.putAll(id, aclService.getTransitions(q).getEpisodeTransistions()); } }).value(); // seriesId -> [transitions] final Map<String, List<SeriesACLTransition>> sTs = sIds.foldl( MultiMap.<String, SeriesACLTransition> multiHashMapWithArrayList(), new Function2.X<MultiMap<String, SeriesACLTransition>, String, MultiMap<String, SeriesACLTransition>>() { @Override public MultiMap<String, SeriesACLTransition> xapply(MultiMap<String, SeriesACLTransition> mmap, String id) throws Exception { // todo it is quite expensive to query each series separately final TransitionQuery q = TransitionQuery.query().withId(id).withScope(AclScope.Series) .withDone(done); return mmap.putAll(id, aclService.getTransitions(q).getSeriesTransistions()); } }).value(); final Jsons.Obj episodesObj = buildEpisodesObj(eTs); final Jsons.Obj seriesObj = buildSeriesObj(sTs); return ok(obj(p("episodes", episodesObj), p("series", seriesObj)).toJson()); } catch (Exception e) { logger.error("Error generating getTransitionsFor response", e); return serverError(); } } @GET @Path("/transitions.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "gettransitionsasjson", description = "Get the transitions as json", returnDescription = "Get the transitions as json", restParameters = { @RestParameter(name = "after", isRequired = false, description = "All transitions with an application date after this one", type = STRING), @RestParameter(name = "before", isRequired = false, description = "All transitions with an application date before this one", type = STRING), @RestParameter(name = "scope", isRequired = false, description = "The transition scope", type = STRING), @RestParameter(name = "id", isRequired = false, description = "The series or episode identifier", type = STRING), @RestParameter(name = "transitionId", isRequired = false, description = "The transition identifier", type = STRING), @RestParameter(name = "managedAclId", isRequired = false, description = "The managed acl identifier", type = INTEGER), @RestParameter(name = "done", isRequired = false, description = "Indicates if already applied", type = BOOLEAN) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The request was processed succesfully"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "Error parsing the given scope"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during processing the request") }) public Response getTransitionsAsJson(@QueryParam("after") String afterStr, @QueryParam("before") String beforeStr, @QueryParam("scope") String scopeStr, @QueryParam("id") String id, @QueryParam("transitionId") Long transitionId, @QueryParam("managedAclId") Long managedAclId, @QueryParam("done") Boolean done) { try { final TransitionQuery query = TransitionQuery.query(); if (StringUtils.isNotBlank(afterStr)) query.after(new Date(DateTimeSupport.fromUTC(afterStr))); if (StringUtils.isNotBlank(beforeStr)) query.before(new Date(DateTimeSupport.fromUTC(beforeStr))); if (StringUtils.isNotBlank(id)) query.withId(id); if (StringUtils.isNotBlank(scopeStr)) { if ("episode".equalsIgnoreCase(scopeStr)) query.withScope(AclScope.Episode); else if ("series".equalsIgnoreCase(scopeStr)) query.withScope(AclScope.Series); else return badRequest(); } if (transitionId != null) query.withTransitionId(transitionId); if (managedAclId != null) query.withAclId(managedAclId); if (done != null) query.withDone(done); final AclService aclService = aclService(); // run query final TransitionResult r = aclService().getTransitions(query); // episodeId -> [transitions] final Map<String, List<EpisodeACLTransition>> episodeGroup = groupByEpisodeId(r.getEpisodeTransistions()); // seriesId -> [transitions] final Map<String, List<SeriesACLTransition>> seriesGroup = groupBySeriesId(r.getSeriesTransistions()); final Jsons.Obj episodesObj = buildEpisodesObj(episodeGroup); final Jsons.Obj seriesObj = buildSeriesObj(seriesGroup); // create final response return ok(obj(p("episodes", episodesObj), p("series", seriesObj)).toJson()); } catch (Exception e) { logger.error("Error generating getTransitions response", e); return serverError(); } } /** Build JSON object for all episodes containing all transitions and the active ACL. */ private Jsons.Obj buildEpisodesObj(Map<String, List<EpisodeACLTransition>> episodeGroup) { final AclService aclService = aclService(); return mlist(episodeGroup.entrySet().iterator()).foldl(obj(), new Function2<Jsons.Obj, Map.Entry<String, List<EpisodeACLTransition>>, Jsons.Obj>() { @Override public Jsons.Obj apply(Jsons.Obj obj, Map.Entry<String, List<EpisodeACLTransition>> ts) { final Jsons.Arr transitions = arr(mlist(ts.getValue()).map(JsonConv.digestEpisodeAclTransition)); final Jsons.Obj activeAcl = buildAclObj(getActiveAclForEpisode(aclService, ts.getKey()), true); return obj.append(buildActiveAclAndTransitionsObj(ts.getKey(), activeAcl, transitions)); } }); } /** Build JSON object for all series containing all transitions and the active ACL. */ private Jsons.Obj buildSeriesObj(Map<String, List<SeriesACLTransition>> seriesGroup) { final AclService aclService = aclService(); return mlist(seriesGroup.entrySet().iterator()).foldl(obj(), new Function2<Jsons.Obj, Map.Entry<String, List<SeriesACLTransition>>, Jsons.Obj>() { @Override public Jsons.Obj apply(Jsons.Obj obj, Map.Entry<String, List<SeriesACLTransition>> ts) { final Jsons.Arr transitions = arr(mlist(ts.getValue()).map(JsonConv.digestSeriesAclTransition)); final Jsons.Obj activeAcl = buildAclObj(getActiveAclForSeries(aclService, ts.getKey()), false); return obj.append(buildActiveAclAndTransitionsObj(ts.getKey(), activeAcl, transitions)); } }); } private static final Jsons.Obj fromSeries = obj(p("isFromSeries", true)); private static final Jsons.Obj fromEpisode = obj(p("isFromSeries", false)); /** Build the JSON obj for un/managed ACLs. */ private Jsons.Obj buildAclObj(final Either<AccessControlList, Tuple<ManagedAcl, AclScope>> acl, final boolean withFlavor) { return acl.fold(nest("unmanagedAcl").o(fullAccessControlList), // managed acl new Function<Tuple<ManagedAcl, AclScope>, Jsons.Obj>() { @Override public Jsons.Obj apply(Tuple<ManagedAcl, AclScope> acl) { final Jsons.Obj digest = digestManagedAcl.apply(acl.getA()); final Jsons.Obj enriched; if (withFlavor) { switch (acl.getB()) { case Episode: enriched = digest.append(fromEpisode); break; case Series: enriched = digest.append(fromSeries); break; default: enriched = unexhaustiveMatch(); } } else { enriched = digest; } return obj(p("managedAcl", enriched)); } }); } /** * Build the obj with active ACL and transitions. * * @param id * either the episode or the series id. */ private Jsons.Obj buildActiveAclAndTransitionsObj(String id, Jsons.Obj activeAcl, Jsons.Arr transitions) { return obj(p(id, obj(p("activeAcl", activeAcl), p("transitions", transitions)))); } /** Group all episode ACL transitions by episode ID. */ private Map<String, List<EpisodeACLTransition>> groupByEpisodeId(List<EpisodeACLTransition> ts) { return mlist(ts) .foldl(MultiMap.<String, EpisodeACLTransition> multiHashMapWithArrayList(), new Function2<MultiMap<String, EpisodeACLTransition>, EpisodeACLTransition, MultiMap<String, EpisodeACLTransition>>() { @Override public MultiMap<String, EpisodeACLTransition> apply(MultiMap<String, EpisodeACLTransition> mmap, EpisodeACLTransition t) { return mmap.put(t.getEpisodeId(), t); } }).value(); } /** Group all series ACL transitions by series ID. */ private Map<String, List<SeriesACLTransition>> groupBySeriesId(List<SeriesACLTransition> ts) { return mlist(ts) .foldl(MultiMap.<String, SeriesACLTransition> multiHashMapWithArrayList(), new Function2<MultiMap<String, SeriesACLTransition>, SeriesACLTransition, MultiMap<String, SeriesACLTransition>>() { @Override public MultiMap<String, SeriesACLTransition> apply(MultiMap<String, SeriesACLTransition> mmap, SeriesACLTransition t) { return mmap.put(t.getSeriesId(), t); } }).value(); } private Either<AccessControlList, Tuple<ManagedAcl, AclScope>> getActiveAclForEpisode(AclService aclService, String episodeId) { final AQueryBuilder q = getAssetManager().createQuery(); final ASelectQuery sq = q.select(q.snapshot()).where(q.mediaPackageId(episodeId).and(q.version().isLatest())); for (Snapshot snapshot : enrich(sq.run()).getSnapshots().head()) { // get active ACL of found media package final Tuple<AccessControlList, AclScope> activeAcl = getAuthorizationService().getActiveAcl(snapshot.getMediaPackage()); // find corresponding managed ACL for (ManagedAcl macl : matchAcls(aclService, activeAcl.getA())) { return right(tuple(macl, activeAcl.getB())); } return left(activeAcl.getA()); } // episode does not exist logger.warn("Episode {} cannot be found in Archive", episodeId); return left(EMPTY_ACL); } private Either<AccessControlList, Tuple<ManagedAcl, AclScope>> getActiveAclForSeries(AclService aclService, String seriesId) { try { final AccessControlList activeAcl = getSeriesService().getSeriesAccessControl(seriesId); for (ManagedAcl macl : matchAcls(aclService, activeAcl)) { return right(tuple(macl, AclScope.Series)); } return left(activeAcl); } catch (NotFoundException e) { // series does not exist logger.warn("Series {} cannot be found in SeriesService", seriesId); } catch (SeriesException e) { logger.error("Error accessing SeriesService", e); return chuck(e); } return left(EMPTY_ACL); } /** Matches the given ACL against all managed ACLs returning the first match. */ private static Option<ManagedAcl> matchAcls(final AclService aclService, final AccessControlList acl) { return mlist(aclService.getAcls()).find(new Predicate<ManagedAcl>() { @Override public Boolean apply(ManagedAcl macl) { return AccessControlUtil.equals(acl, macl.getAcl()); } }); } @GET @Path("/acl/{aclId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getacl", description = "Return the ACL by the given id", returnDescription = "Return the ACL by the given id", pathParameters = { @RestParameter(name = "aclId", isRequired = true, description = "The ACL identifier", type = INTEGER) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during returning the ACL") }) public String getAcl(@PathParam("aclId") long aclId) throws NotFoundException { final Option<ManagedAcl> managedAcl = aclService().getAcl(aclId); if (managedAcl.isNone()) { logger.info("No ACL with id '{}' could be found", aclId); throw new NotFoundException(); } return JsonConv.full(managedAcl.get()).toJson(); } @POST @Path("/acl/extend") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "extendacl", description = "Return the given ACL with a new role and action in JSON format", returnDescription = "Return the ACL with the new role and action in JSON format", restParameters = { @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING), @RestParameter(name = "action", isRequired = true, description = "The action for the ACL", type = STRING), @RestParameter(name = "role", isRequired = true, description = "The role for the ACL", type = STRING), @RestParameter(name = "allow", isRequired = true, description = "The allow status for the ACL", type = BOOLEAN) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "The ACL, action or role was invalid or empty"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during returning the ACL") }) public String extendAcl(@FormParam("acl") String accessControlList, @FormParam("action") String action, @FormParam("role") String role, @FormParam("allow") boolean allow) { if (StringUtils.isBlank(accessControlList) || StringUtils.isBlank(action) || StringUtils.isBlank(role)) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } AccessControlList acl = AccessControlUtil.extendAcl(parseAcl.apply(accessControlList), role, action, allow); return JsonConv.full(acl).toJson(); } @POST @Path("/acl/reduce") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "reduceacl", description = "Return the given ACL without a role and action in JSON format", returnDescription = "Return the ACL without the role and action in JSON format", restParameters = { @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING), @RestParameter(name = "action", isRequired = true, description = "The action for the ACL", type = STRING), @RestParameter(name = "role", isRequired = true, description = "The role for the ACL", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been returned"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "The ACL, role or action was invalid or empty"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during returning the ACL") }) public String reduceAcl(@FormParam("acl") String accessControlList, @FormParam("action") String action, @FormParam("role") String role) { if (StringUtils.isBlank(accessControlList) || StringUtils.isBlank(action) || StringUtils.isBlank(role)) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } AccessControlList acl = AccessControlUtil.reduceAcl(parseAcl.apply(accessControlList), role, action); return JsonConv.full(acl).toJson(); } @GET @Path("/acl/acls.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getacls", description = "Lists the ACL's as JSON", returnDescription = "The list of ACL's as JSON", reponses = { @RestResponse(responseCode = SC_OK, description = "The list of ACL's has successfully been returned"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during returning the list of ACL's") }) public String getAcls() { return Jsons.arr(mlist(aclService().getAcls()).map(Functions.co(JsonConv.fullManagedAcl))) .toJson(); } @POST @Path("/acl") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "createacl", description = "Create an ACL", returnDescription = "Create an ACL", restParameters = { @RestParameter(name = "name", isRequired = true, description = "The ACL name", type = STRING), @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been added"), @RestResponse(responseCode = SC_CONFLICT, description = "An ACL with the same name already exists"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the ACL"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during adding the ACL") }) public String createAcl(@FormParam("name") String name, @FormParam("acl") String accessControlList) { final AccessControlList acl = parseAcl.apply(accessControlList); final Option<ManagedAcl> managedAcl = aclService().createAcl(acl, name); if (managedAcl.isNone()) { logger.info("An ACL with the same name '{}' already exists", name); throw new WebApplicationException(Response.Status.CONFLICT); } return JsonConv.full(managedAcl.get()).toJson(); } @PUT @Path("/acl/{aclId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "updateacl", description = "Update an ACL", returnDescription = "Update an ACL", pathParameters = { @RestParameter(name = "aclId", isRequired = true, description = "The ACL identifier", type = INTEGER) }, restParameters = { @RestParameter(name = "name", isRequired = true, description = "The ACL name", type = STRING), @RestParameter(name = "acl", isRequired = true, description = "The access control list", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been updated"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the ACL"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during updating the ACL") }) public String updateAcl(@PathParam("aclId") long aclId, @FormParam("name") String name, @FormParam("acl") String accessControlList) throws NotFoundException { final Organization org = getSecurityService().getOrganization(); final AccessControlList acl = parseAcl.apply(accessControlList); final ManagedAclImpl managedAcl = new ManagedAclImpl(aclId, name, org.getId(), acl); if (!aclService().updateAcl(managedAcl)) { logger.info("No ACL with id '{}' could be found under organization '{}'", aclId, org.getId()); throw new NotFoundException(); } return JsonConv.full(managedAcl).toJson(); } @DELETE @Path("/acl/{aclId}") @RestQuery(name = "deleteacl", description = "Delete an ACL", returnDescription = "Delete an ACL", pathParameters = { @RestParameter(name = "aclId", isRequired = true, description = "The ACL identifier", type = INTEGER) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has successfully been deleted"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL has not been found"), @RestResponse(responseCode = SC_CONFLICT, description = "The ACL could not be deleted, there are still references on it"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error during deleting the ACL") }) public Response deleteAcl(@PathParam("aclId") long aclId) throws NotFoundException { try { if (!aclService().deleteAcl(aclId)) return conflict(); } catch (AclServiceException e) { logger.warn("Error deleting manged acl with id '{}': {}", aclId, e); throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); } return noContent(); } @POST @Path("/apply/episode/{episodeId}") @RestQuery(name = "applyAclToEpisode", description = "Immediate application of an ACL to an episode", returnDescription = "Status code", pathParameters = { @RestParameter(name = "episodeId", isRequired = true, description = "The episode ID", type = STRING) }, restParameters = { @RestParameter(name = "aclId", isRequired = false, description = "The ID of the ACL to apply. If missing the episode ACL will be deleted to fall back to the series ACL", type = INTEGER), @RestParameter(name = "workflowDefinitionId", isRequired = false, description = "The optional workflow to apply to the episode after", type = STRING), @RestParameter(name = "workflowParams", isRequired = false, description = "Parameters for the optional workflow", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL or the episode has not been found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error") }) public Response applyAclToEpisode(@PathParam("episodeId") String episodeId, @FormParam("aclId") Long aclId, @FormParam("workflowDefinitionId") String workflowDefinitionId, @FormParam("workflowParams") String workflowParams) { final AclService aclService = aclService(); final Option<Option<ManagedAcl>> macl = option(aclId).map(getManagedAcl(aclService)); if (macl.isSome() && macl.get().isNone()) return notFound(); final Option<ConfiguredWorkflowRef> workflow = createConfiguredWorkflowRef(workflowDefinitionId, workflowParams); try { if (aclService.applyAclToEpisode(episodeId, Options.join(macl), workflow)) return ok(); else return notFound(); } catch (AclServiceException e) { logger.error("Error applying acl to episode {}", episodeId); return serverError(); } } @POST @Path("/apply/series/{seriesId}") @RestQuery(name = "applyAclToSeries", description = "Immediate application of an ACL to a series", returnDescription = "Status code", pathParameters = { @RestParameter(name = "seriesId", isRequired = true, description = "The series ID", type = STRING) }, restParameters = { @RestParameter(name = "aclId", isRequired = true, description = "The ID of the ACL to apply", type = INTEGER), @RestParameter(name = "override", isRequired = false, defaultValue = "false", description = "If true the series ACL will take precedence over any existing episode ACL", type = STRING), @RestParameter(name = "workflowDefinitionId", isRequired = false, description = "The optional workflow to apply to the series after", type = STRING), @RestParameter(name = "workflowParams", isRequired = false, description = "Parameters for the optional workflow", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The ACL or the series has not been found"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error") }) public Response applyAclToSeries(@PathParam("seriesId") String seriesId, @FormParam("aclId") long aclId, @DefaultValue("false") @FormParam("override") boolean override, @FormParam("workflowDefinitionId") String workflowDefinitionId, @FormParam("workflowParams") String workflowParams) { final AclService aclService = aclService(); for (ManagedAcl macl : aclService.getAcl(aclId)) { final Option<ConfiguredWorkflowRef> workflow = createConfiguredWorkflowRef(workflowDefinitionId, workflowParams); try { if (aclService.applyAclToSeries(seriesId, macl, override, workflow)) return ok(); else return notFound(); } catch (AclServiceException e) { logger.error("Error applying acl to series {}", seriesId); return serverError(); } } // acl not found return notFound(); } /** Create a ConfiguredWorkflowRef from raw request strings that may be null. */ private static Option<ConfiguredWorkflowRef> createConfiguredWorkflowRef(String workflowId, String workflowParamsJson) { return Util.createConfiguredWorkflowRef(option(workflowId).bind(trimToNone), option(workflowParamsJson).bind(trimToNone)); } private static final Function<String, AccessControlList> parseAcl = new Function<String, AccessControlList>() { @Override public AccessControlList apply(String acl) { try { return AccessControlParser.parseAcl(acl); } catch (Exception e) { logger.warn("Unable to parse ACL"); throw new WebApplicationException(Response.Status.BAD_REQUEST); } } }; private AclService aclService() { return getAclServiceFactory().serviceFor(getSecurityService().getOrganization()); } }