CVE-2021-21318 (Java, published 2021-02-18T18:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 5.4)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/opencast/opencast
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 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.


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);