CVE-2021-21318: Incorrect Authorization (CWE-863)
value: Opencast is a free, open-source platform to support the management of educational audio and video content. In Opencast before version 9.2 there is a vulnerability in which publishing an episode with strict access rules will overwrite the currently set series access. This allows for an easy denial of access for all users without superuser privileges, effectively hiding the series. 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. Affected versions of Opencast may not update the series access or remove a published series if an event is being removed. On removal of an episode, this may lead to an access control list for series metadata with broader access rules than the merged access rules of all remaining events, or the series metadata still being available although all episodes of that series have been removed. This problem is fixed in Opencast 9.2.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 5.4
Published: 2021-02-18T18:15Z
Repository: https://github.com/opencast/opencast
Language: Java
Files changed: modules/search-service-impl/src/main/java/org/opencastproject/search/impl/SearchServiceImpl.java, modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchEntity.java, modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabase.java, modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabaseImpl.java, modules/search-service-impl/src/main/java/org/opencastproject/search/impl/solr/SolrIndexManager.java
Commit date: 2021-01-17 17:33:25 +0100
Commit message: 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.

Diff: diff --git a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/SearchServiceImpl.java b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/SearchServiceImpl.java
index ddc648759d..b8d426c836 100644
--- a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/SearchServiceImpl.java
+++ b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/SearchServiceImpl.java
@@ -371,42 +371,46 @@ public final class SearchServiceImpl extends AbstractJobProducer implements Sear
    *
    * @param mediaPackage
    *          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);
     }
 
     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);
     }
   }
 
   /**
    * {@inheritDoc}
@@ -427,27 +431,23 @@ public final class SearchServiceImpl extends AbstractJobProducer implements Sear
    * @param mediaPackageId
    *          the mediapackage
    * @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));
       if (result.getItems().length == 0) {
         logger.warn(
                 "Can not delete mediapackage {}, which is not available for the current user to delete from the search index.",
                 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 {
         persistence.deleteMediaPackage(mediaPackageId, now);
         logger.info("Removed mediapackage {} from search persistence", mediaPackageId);
@@ -458,12 +458,28 @@ public final class SearchServiceImpl extends AbstractJobProducer implements Sear
       } catch (SearchServiceDatabaseException e) {
         logger.error("Could not delete media package with id {} from persistence storage", mediaPackageId);
         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);
     }
   }
 
diff --git a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchEntity.java b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchEntity.java
index e25caec4d3..9715322b7f 100644
--- a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchEntity.java
+++ b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchEntity.java
@@ -48,11 +48,12 @@ import javax.persistence.TemporalType;
     @Index(name = "IX_oc_search_organization", columnList = ("organization")) })
 @NamedQueries({
     @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 {
 
   /** media package id, primary key */
   @Id
diff --git a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabase.java b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabase.java
index 8e117d84fe..b0dbbe562d 100644
--- a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabase.java
+++ b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabase.java
@@ -25,10 +25,11 @@ import org.opencastproject.mediapackage.MediaPackage;
 import org.opencastproject.security.api.AccessControlList;
 import org.opencastproject.security.api.UnauthorizedException;
 import org.opencastproject.util.NotFoundException;
 import org.opencastproject.util.data.Tuple;
 
+import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
 
 /**
  * API that defines persistent storage of series.
@@ -79,22 +80,47 @@ public interface SearchServiceDatabase {
    *           if there is a problem communicating with the underlying data store
    */
   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
    * @return {@link AccessControlList} of media package or null if media package does not have ACL associated with it
    * @throws NotFoundException
    *           if media package with given ID does not exist
    * @throws SearchServiceDatabaseException
    *           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.
    *
    * @param mediaPackageId
diff --git a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabaseImpl.java b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabaseImpl.java
index 4d002e06fe..69bd46e555 100644
--- a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabaseImpl.java
+++ b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/persistence/SearchServiceDatabaseImpl.java
@@ -24,13 +24,15 @@ package org.opencastproject.search.impl.persistence;
 import static org.opencastproject.security.api.Permissions.Action.CONTRIBUTE;
 import static org.opencastproject.security.api.Permissions.Action.READ;
 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;
 import org.opencastproject.security.api.UnauthorizedException;
 import org.opencastproject.security.api.User;
@@ -40,10 +42,14 @@ import org.opencastproject.util.data.Tuple;
 import org.apache.commons.lang3.StringUtils;
 import org.osgi.service.component.ComponentContext;
 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;
 import java.util.List;
 
@@ -248,17 +254,70 @@ public class SearchServiceDatabaseImpl implements SearchServiceDatabase {
         return AccessControlParser.parseAcl(entity.getAccessControl());
       }
     } 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}
    *
    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#storeMediaPackage(MediaPackage,
    *      AccessControlList, Date)
diff --git a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/solr/SolrIndexManager.java b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/solr/SolrIndexManager.java
index dacfbd849d..6980d03863 100644
--- a/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/solr/SolrIndexManager.java
+++ b/modules/search-service-impl/src/main/java/org/opencastproject/search/impl/solr/SolrIndexManager.java
@@ -271,36 +271,51 @@ public class SolrIndexManager {
    * @param now
    *          current date
    * @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);
     }
   }
 
   /**
    * Posts the media package to solr. Depending on what is referenced in the media package, the method might create one

Vulnerable code:    * @throws MediaPackageException
   *           if the mediapckage is invalid
  public void addSynchronously(MediaPackage mediaPackage) throws SearchException, MediaPackageException,
          IllegalArgumentException, UnauthorizedException {
    logger.debug("Attempting to add mediapackage {} to search index", mediaPackage.getIdentifier());
      if (indexManager.add(mediaPackage, acl, now)) {
        logger.info("Added mediapackage `{}` to the search index, using ACL `{}`", mediaPackage, acl);
        logger.warn("Failed to add mediapackage {} to the search index", mediaPackage.getIdentifier());
      logger.error("Could not store media package to search database {}: {}", mediaPackage.getIdentifier(), e);
      throw new SearchException(e);
   * @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 {
      logger.info("Removing mediapackage {} from search index", mediaPackageId);
      return indexManager.delete(mediaPackageId, now);
    } catch (SolrServerException e) {
    @NamedQuery(name = "Search.findBySeriesId", query = "SELECT s FROM SearchEntity s WHERE s.seriesId=:seriesId"),
   * Retrieves ACL for series with given ID.
          SearchServiceDatabaseException;
      logger.error("Could not retrieve ACL {}: {}", mediaPackageId, e.getMessage());
  public boolean add(MediaPackage sourceMediaPackage, AccessControlList acl, Date now) throws SolrServerException,
          UnauthorizedException {
      SolrInputDocument seriesDocument = createSeriesInputDocument(sourceMediaPackage.getSeries(), acl);
      // 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;
      }

      if (episodeDocument != null)
        solrServer.add(episodeDocument);
      logger.error("Unable to add mediapackage {} to index", sourceMediaPackage.getIdentifier());
      throw new SolrServerException(e);
Fixed code:   public void addSynchronously(MediaPackage mediaPackage)
      throws SearchException, IllegalArgumentException, UnauthorizedException, NotFoundException,
      SearchServiceDatabaseException {
    final String mediaPackageId = mediaPackage.getIdentifier().toString();
    logger.debug("Attempting to add media package {} to search index", mediaPackageId);
    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);

      if (indexManager.add(mediaPackage, acl, seriesAcl, now)) {
        logger.info("Added media package `{}` to the search index, using ACL `{}`", mediaPackageId, acl);
        logger.warn("Failed to add media package {} to the search index", mediaPackageId);
      throw new SearchException(
          String.format("Could not store media package to search database %s", mediaPackageId), e);
  public boolean deleteSynchronously(final String mediaPackageId) throws SearchException {
      final String seriesId = result.getItems()[0].getDcIsPartOf();
      logger.info("Removing media package {} from search index", mediaPackageId);
      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) {
    @NamedQuery(name = "Search.findBySeriesId", query = "SELECT s FROM SearchEntity s WHERE s.seriesId=:seriesId and "
        + "s.deletionDate is null"),
import java.util.Collection;
   * 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.
      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;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.security.api.AccessControlParsingException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
      logger.error("Could not retrieve ACL {}", mediaPackageId, e);
  /**
   * {@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;
  }

  public boolean add(MediaPackage sourceMediaPackage, AccessControlList acl, AccessControlList seriesAcl, Date now)
      throws SolrServerException, UnauthorizedException {
      SolrInputDocument seriesDocument = createSeriesInputDocument(sourceMediaPackage.getSeries(), seriesAcl);
      solrServer.add(episodeDocument);
      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);