Skip to content
Permalink
Browse files Browse the repository at this point in the history
Fix Engage Series Publication and Access
Access to series and series metadata on the search service (shown in
media module and player) depends on the events published which are part
of the series. Publishing an event will automatically publish a series
and update access to it. Removing an event or republishing the event
should do the same.

Incorrectly Hiding Public Series
--------------------------------

This patch fixes the access control update to the series when a new
episode is being published. Until now, a new episode publication would
always update the series access with the episode access.

While this is no security issue since it can only cause the access to be
stricter, it may cause public series to become private. This would
happen, for example, if a users sets one episode of a series to private
and republishes the episode.

Now, the search service will merge the access control lists of all
episodes to grant access based on their combined access rules.

Update Series on Removal
------------------------

This patch fixes Opencast not updating the series access or remove a
published series if an event is being removed.

This means that access to a series is re-calculated when an episode is
being deleted based on the remaining published episodes in the series.
For example, removing the last episode with public access will now make
the series private which it would have stayed public before.

It also means that if the last episode of a series is being removed, the
series itself will be unpublished as well, so no empty series will
continue to show up any longer.
  • Loading branch information
lkiesow committed Jan 17, 2021
1 parent 29b08ce commit b18c6a7
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 36 deletions.
Expand Up @@ -373,28 +373,32 @@ public Job add(MediaPackage mediaPackage) throws SearchException, MediaPackageEx
* the media package
* @throws SearchException
* if the media package cannot be added to the search index
* @throws MediaPackageException
* if the mediapckage is invalid
* @throws IllegalArgumentException
* if the mediapackage is <code>null</code>
* @throws UnauthorizedException
* if the user does not have the rights to add the mediapackage
*/
public void addSynchronously(MediaPackage mediaPackage) throws SearchException, MediaPackageException,
IllegalArgumentException, UnauthorizedException {
public void addSynchronously(MediaPackage mediaPackage)
throws SearchException, IllegalArgumentException, UnauthorizedException, NotFoundException,
SearchServiceDatabaseException {
if (mediaPackage == null) {
throw new IllegalArgumentException("Unable to add a null mediapackage");
}
logger.debug("Attempting to add mediapackage {} to search index", mediaPackage.getIdentifier());
final String mediaPackageId = mediaPackage.getIdentifier().toString();
logger.debug("Attempting to add media package {} to search index", mediaPackageId);
AccessControlList acl = authorizationService.getActiveAcl(mediaPackage).getA();

AccessControlList seriesAcl = persistence.getAccessControlLists(mediaPackage.getSeries(), mediaPackageId).stream()
.reduce(new AccessControlList(acl.getEntries()), AccessControlList::mergeActions);
logger.debug("Updating series with merged access control list: {}", seriesAcl);

Date now = new Date();

try {
if (indexManager.add(mediaPackage, acl, now)) {
logger.info("Added mediapackage `{}` to the search index, using ACL `{}`", mediaPackage, acl);
if (indexManager.add(mediaPackage, acl, seriesAcl, now)) {
logger.info("Added media package `{}` to the search index, using ACL `{}`", mediaPackageId, acl);
} else {
logger.warn("Failed to add mediapackage {} to the search index", mediaPackage.getIdentifier());
logger.warn("Failed to add media package {} to the search index", mediaPackageId);
}
} catch (SolrServerException e) {
throw new SearchException(e);
Expand All @@ -403,8 +407,8 @@ public void addSynchronously(MediaPackage mediaPackage) throws SearchException,
try {
persistence.storeMediaPackage(mediaPackage, acl, now);
} catch (SearchServiceDatabaseException e) {
logger.error("Could not store media package to search database {}: {}", mediaPackage.getIdentifier(), e);
throw new SearchException(e);
throw new SearchException(
String.format("Could not store media package to search database %s", mediaPackageId), e);
}
}

Expand All @@ -429,13 +433,8 @@ public Job delete(String mediaPackageId) throws SearchException, UnauthorizedExc
* @return <code>true</code> if the mediapackage was deleted
* @throws SearchException
* if deletion failed
* @throws UnauthorizedException
* if the user did not have access to the media package
* @throws NotFoundException
* if the mediapackage did not exist
*/
public boolean deleteSynchronously(String mediaPackageId) throws SearchException, UnauthorizedException,
NotFoundException {
public boolean deleteSynchronously(final String mediaPackageId) throws SearchException {
SearchResult result;
try {
result = solrRequester.getForWrite(new SearchQuery().withId(mediaPackageId));
Expand All @@ -445,7 +444,8 @@ public boolean deleteSynchronously(String mediaPackageId) throws SearchException
mediaPackageId);
return false;
}
logger.info("Removing mediapackage {} from search index", mediaPackageId);
final String seriesId = result.getItems()[0].getDcIsPartOf();
logger.info("Removing media package {} from search index", mediaPackageId);

Date now = new Date();
try {
Expand All @@ -460,8 +460,24 @@ public boolean deleteSynchronously(String mediaPackageId) throws SearchException
throw new SearchException(e);
}

return indexManager.delete(mediaPackageId, now);
} catch (SolrServerException e) {
final boolean success = indexManager.delete(mediaPackageId, now);

// Update series
if (seriesId != null) {
if (persistence.getMediaPackages(seriesId).size() > 0) {
// Update series acl if there are still episodes in the series
final AccessControlList seriesAcl = persistence.getAccessControlLists(seriesId).stream()
.reduce(new AccessControlList(), AccessControlList::mergeActions);
indexManager.addSeries(seriesId, seriesAcl);

} else {
// Remove series if there are no episodes in the series any longer
indexManager.delete(seriesId, now);
}
}

return success;
} catch (SolrServerException | SearchServiceDatabaseException e) {
logger.info("Could not delete media package with id {} from search index", mediaPackageId);
throw new SearchException(e);
}
Expand Down
Expand Up @@ -50,7 +50,8 @@
@NamedQuery(name = "Search.findAll", query = "SELECT s FROM SearchEntity s"),
@NamedQuery(name = "Search.getCount", query = "SELECT COUNT(s) FROM SearchEntity s"),
@NamedQuery(name = "Search.findById", query = "SELECT s FROM SearchEntity s WHERE s.mediaPackageId=:mediaPackageId"),
@NamedQuery(name = "Search.findBySeriesId", query = "SELECT s FROM SearchEntity s WHERE s.seriesId=:seriesId"),
@NamedQuery(name = "Search.findBySeriesId", query = "SELECT s FROM SearchEntity s WHERE s.seriesId=:seriesId and "
+ "s.deletionDate is null"),
@NamedQuery(name = "Search.getNoSeries", query = "SELECT s FROM SearchEntity s WHERE s.seriesId IS NULL")})
public class SearchEntity {

Expand Down
Expand Up @@ -27,6 +27,7 @@
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.data.Tuple;

import java.util.Collection;
import java.util.Date;
import java.util.Iterator;

Expand Down Expand Up @@ -81,7 +82,18 @@ public interface SearchServiceDatabase {
MediaPackage getMediaPackage(String mediaPackageId) throws NotFoundException, SearchServiceDatabaseException;

/**
* Retrieves ACL for series with given ID.
* Gets media packages from a specific series
*
* @param seriesId
* the series identifier
* @return collection of media packages
* @throws SearchServiceDatabaseException
* if there is a problem communicating with the underlying data store
*/
Collection<MediaPackage> getMediaPackages(String seriesId) throws SearchServiceDatabaseException;

/**
* Retrieves ACL for episode with given ID.
*
* @param mediaPackageId
* media package for which ACL will be retrieved
Expand All @@ -92,7 +104,21 @@ public interface SearchServiceDatabase {
* if exception occurred
*/
AccessControlList getAccessControlList(String mediaPackageId) throws NotFoundException,
SearchServiceDatabaseException;
SearchServiceDatabaseException;

/**
* Retrieves ACLs for series with given ID.
*
* @param seriesId
* series identifier for which ACL will be retrieved
* @param excludeIds
* list of media package identifier to exclude from the list
* @return Collection of {@link AccessControlList} of media packages from the series
* @throws SearchServiceDatabaseException
* if exception occurred
*/
Collection<AccessControlList> getAccessControlLists(String seriesId, String ... excludeIds)
throws SearchServiceDatabaseException;

/**
* Returns the modification date from the selected media package.
Expand Down
Expand Up @@ -26,9 +26,11 @@
import static org.opencastproject.security.api.Permissions.Action.WRITE;

import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageParser;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.AccessControlParsingException;
import org.opencastproject.security.api.AccessControlUtil;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.SecurityService;
Expand All @@ -42,6 +44,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
Expand Down Expand Up @@ -250,13 +256,66 @@ public AccessControlList getAccessControlList(String mediaPackageId) throws NotF
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Could not retrieve ACL {}: {}", mediaPackageId, e.getMessage());
logger.error("Could not retrieve ACL {}", mediaPackageId, e);
throw new SearchServiceDatabaseException(e);
} finally {
em.close();
}
}

/**
* {@inheritDoc}
*
* @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getAccessControlLists(String, String...)
*/
@Override
public Collection<AccessControlList> getAccessControlLists(final String seriesId, String ... excludeIds)
throws SearchServiceDatabaseException {
List<String> excludes = Arrays.asList(excludeIds);
List<AccessControlList> accessControlLists = new ArrayList<>();
EntityManager em = emf.createEntityManager();
TypedQuery<SearchEntity> q = em.createNamedQuery("Search.findBySeriesId", SearchEntity.class)
.setParameter("seriesId", seriesId);
try {
for (SearchEntity entity: q.getResultList()) {
if (entity.getAccessControl() != null && !excludes.contains(entity.getMediaPackageId())) {
accessControlLists.add(AccessControlParser.parseAcl(entity.getAccessControl()));
}
}
} catch (IOException | AccessControlParsingException e) {
throw new SearchServiceDatabaseException(e);
} finally {
em.close();
}
return accessControlLists;
}

/**
* {@inheritDoc}
*
* @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getMediaPackages(String)
*/
@Override
public Collection<MediaPackage> getMediaPackages(final String seriesId)
throws SearchServiceDatabaseException {
List<MediaPackage> episodes = new ArrayList<>();
EntityManager em = emf.createEntityManager();
TypedQuery<SearchEntity> q = em.createNamedQuery("Search.findBySeriesId", SearchEntity.class)
.setParameter("seriesId", seriesId);
try {
for (SearchEntity entity: q.getResultList()) {
if (entity.getMediaPackageXML() != null) {
episodes.add(MediaPackageParser.getFromXml(entity.getMediaPackageXML()));
}
}
} catch (MediaPackageException e) {
throw new SearchServiceDatabaseException(e);
} finally {
em.close();
}
return episodes;
}

/**
* {@inheritDoc}
*
Expand Down
Expand Up @@ -273,32 +273,47 @@ public boolean delete(String id, Date deletionDate) throws SolrServerException {
* @throws SolrServerException
* if an errors occurs while talking to solr
*/
public boolean add(MediaPackage sourceMediaPackage, AccessControlList acl, Date now) throws SolrServerException,
UnauthorizedException {
public boolean add(MediaPackage sourceMediaPackage, AccessControlList acl, AccessControlList seriesAcl, Date now)
throws SolrServerException, UnauthorizedException {
try {
SolrInputDocument episodeDocument = createEpisodeInputDocument(sourceMediaPackage, acl);
Schema.setOcModified(episodeDocument, now);

SolrInputDocument seriesDocument = createSeriesInputDocument(sourceMediaPackage.getSeries(), acl);
SolrInputDocument seriesDocument = createSeriesInputDocument(sourceMediaPackage.getSeries(), seriesAcl);
if (seriesDocument != null)
Schema.enrich(episodeDocument, seriesDocument);

// If neither an episode nor a series was contained, there is no point in trying to update
if (episodeDocument == null && seriesDocument == null) {
logger.warn("Neither episode nor series metadata found");
return false;
}

// Post everything to the search index
if (episodeDocument != null)
solrServer.add(episodeDocument);
solrServer.add(episodeDocument);
if (seriesDocument != null)
solrServer.add(seriesDocument);
solrServer.commit();
return true;
} catch (Exception e) {
logger.error("Unable to add mediapackage {} to index", sourceMediaPackage.getIdentifier());
throw new SolrServerException(e);
throw new SolrServerException(
String.format("Unable to add media package %s to index", sourceMediaPackage.getIdentifier()), e);
}
}

/**
* Posts a series to Solr. If the entry already exists, this will update the series.
*
* @param seriesId
* the series to post
* @param acl
* the access control list for this series
* @throws SolrServerException
* if an errors occurs while talking to solr
*/
public void addSeries(final String seriesId, final AccessControlList acl) throws SolrServerException {
try {
SolrInputDocument seriesDocument = createSeriesInputDocument(seriesId, acl);
if (seriesDocument != null) {
solrServer.add(seriesDocument);
solrServer.commit();
}
} catch (Exception e) {
throw new SolrServerException(String.format("Unable to add series %s to index", seriesId), e);
}
}

Expand Down

0 comments on commit b18c6a7

Please sign in to comment.