AclScanner.java

/*
 * 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.impl;

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.ManagedAcl;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.AccessControlParsingException;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.util.NotFoundException;

import org.apache.commons.io.FilenameUtils;
import org.apache.felix.fileinstall.ArtifactInstaller;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Component(
    immediate = true,
    service = { ArtifactInstaller.class, AclScanner.class },
    property = {
        "service.description=Acl Scanner"
    }
)
public class AclScanner implements ArtifactInstaller {

  /** The directory name that holds the ACL files **/
  public static final String ACL_DIRECTORY = "acl";

  /** The logging instance */
  private static final Logger logger = LoggerFactory.getLogger(AclScanner.class);

  /** The organization directory service */
  private OrganizationDirectoryService organizationDirectoryService;

  /** The Access Control service */
  private AclServiceFactory aclServiceFactory;

  private SecurityService securityService;

  /**
   * A map linking the Acl file name concatenate with the organization id {@code filename_organizationId} to the related
   * managed Acl Id
   */
  private final Map<String, Long> managedAcls = new HashMap<>();

  /**
   * OSGI service activation method
   */
  @Activate
  void activate(BundleContext ctx) {
    logger.info("Activated Acl scanner");
  }

  /**
   * OSGI deactivation method
   */
  @Deactivate
  void deactivate(BundleContext ctx) {
    logger.info("Deactivated Acl scanner");
  }

  /** OSGi DI. */
  @Reference
  void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
    this.organizationDirectoryService = organizationDirectoryService;
  }

  /** OSGi callback for setting persistence. */
  @Reference
  void setAclServiceFactory(AclServiceFactory aclServiceFactory) {
    this.aclServiceFactory = aclServiceFactory;
  }

  /** OSGi DI */
  @Reference
  void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  /**
   * {@inheritDoc}
   *
   * @see org.apache.felix.fileinstall.ArtifactListener#canHandle(java.io.File)
   */
  @Override
  public boolean canHandle(File artifact) {
    return ACL_DIRECTORY.equals(artifact.getParentFile().getName()) && artifact.getName().endsWith(".json");
  }

  /**
   * Add ACL from configuration directory to all organizations.
   *
   * @param artifact
   *          The File representing the ACL.
   * @throws IOException
   * @throws AccessControlParsingException
   */
  private void addAcl(File artifact) throws IOException, AccessControlParsingException {
    List<Organization> organizations = organizationDirectoryService.getOrganizations();

    logger.debug("Adding Acl {}", artifact.getAbsolutePath());

    String fileName = FilenameUtils.removeExtension(artifact.getName());
    AccessControlList acl = parseToAcl(artifact);
    Optional<ManagedAcl> managedAcl;

    // Add the Acl to all the organizations
    for (Organization org : organizations) {
      securityService.setOrganization(org);
      // If there are already (not-default) Acl defined for this organization, we skip this one.
      boolean skip = false;
      for (ManagedAcl a : getAclService(org).getAcls()) {
        if (managedAcls.get(generateAclId(a.getName(), org)) == null) {
          logger.debug(
                  "The Acl {} will be not added to the organisation {} as it already contains other not-default Acls.",
                  fileName, org.getName());
          skip = true;
          continue;
        }
      }
      if (!skip) {
        managedAcl = getAclService(org).createAcl(acl, fileName);
        if (managedAcl.isPresent()) {
          managedAcls.put(generateAclId(fileName, org), managedAcl.get().getId());
          logger.debug("Acl from '{}' has been added for the organisation {}", fileName, org.getName());
        } else {
          logger.debug("Acl from '{}' has already been added to the organisation {}.", fileName, org.getName());
        }
      }
    }
  }

  /**
   * Update ACL from configuration directory in all organizations.
   *
   * @param artifact
   *          The File representing the ACL.
   * @throws IOException
   * @throws AccessControlParser
   */
  private void updateAcl(File artifact) throws IOException, AccessControlParsingException {
    List<Organization> organizations = organizationDirectoryService.getOrganizations();

    logger.debug("Updating Acl {}", artifact.getAbsolutePath());

    String fileName = FilenameUtils.removeExtension(artifact.getName());
    AccessControlList acl = parseToAcl(artifact);

    // Update the Acl on all the organizations
    for (Organization org : organizations) {
      securityService.setOrganization(org);
      Long id = managedAcls.get(generateAclId(fileName, org));
      if (id != null) {
        // If the Acl Id is in the managedAcls map, we update the Acl
        if (!getAclService(org).updateAcl(new ManagedAclImpl(id, fileName, org.getId(), acl))) {
          logger.warn("No Acl found with the id {} for the organisation {}.", id, org.getName());
        } else {
          logger.debug("Acl from file {} has been updated for the organisation {}", fileName, org.getName());
        }
      } else {
        logger.info("The ACL file {} has not been added to the organisation {} and will therefore not be updated",
                fileName, org.getName());
      }
    }
  }

  /**
   * Remove ACL from configuration directory from all the organizations.
   *
   * @param artifact
   *          The File representing the ACL.
   */
  private void removeAcl(File artifact) {
    List<Organization> organizations = organizationDirectoryService.getOrganizations();

    logger.debug("Removing Acl {}", artifact.getAbsolutePath());

    String fileName = FilenameUtils.removeExtension(artifact.getName());

    // Remove the Acl on all the organizations
    for (Organization org : organizations) {
      securityService.setOrganization(org);
      Long id = managedAcls.get(generateAclId(fileName, org));
      if (id != null) {
        try {
          getAclService(org).deleteAcl(id);
        } catch (NotFoundException e) {
          logger.debug("Unable to delete managed acl {}. Managed acl already deleted!", id);
        } catch (AclServiceException e) {
          logger.error("Unable to delete managed acl {}", id, e);
        }
      } else {
        logger.debug("No Acl matching file {} found.", fileName);
      }
    }
  }

  /**
   * Generate an Acl Id from ACL filename and organization id
   *
   * @param fileName
   *          the ACL file
   * @param org
   *          the organization
   * @return the generated Acl Id
   */
  private String generateAclId(String fileName, Organization org) {
    return fileName + "_" + org.getId();
  }

  /**
   * Parse the given ACL JSON file into an Access Control List
   *
   * @param artifact JSON file
   * @return Parsed access control list
   * @throws IOException Error accessing the ACL file
   * @throws AccessControlParsingException Error parsing the ACL files
   */
  private AccessControlList parseToAcl(File artifact)
          throws IOException, AccessControlParsingException {
    try (FileInputStream in = new FileInputStream(artifact)) {
      return AccessControlParser.parseAcl(in);
    }
  }

  private AclService getAclService(Organization organization) {
    return aclServiceFactory.serviceFor(organization);
  }

  @Override
  public void install(File artifact) throws Exception {
    logger.info("Installing Acl {}", artifact.getAbsolutePath());
    addAcl(artifact);
  }

  @Override
  public void update(File artifact) throws Exception {
    logger.info("Updating Acl {}", artifact.getAbsolutePath());
    updateAcl(artifact);
  }

  @Override
  public void uninstall(File artifact) throws Exception {
    logger.info("Removing Acl {}", artifact.getAbsolutePath());
    removeAcl(artifact);
  }
}