asulibraries / islandora-repo

ASU Digital Repository on Islandora
GNU General Public License v2.0
4 stars 4 forks source link

Cantaloupe Permissions #140

Open elizoller opened 4 years ago

elizoller commented 4 years ago

As a result of implementing a permissions structure and having unpublished content, I realized an interesting thing: cantaloupe interacts with the binaries from drupal (via the flysystem) as an anonymous user. This means that if you are displaying an openseadragon or mirador viewer on a page as a logged in user for an item that you have access to but is not accessible by an anonymous user, the cantaloupe call will result in a 403 meaning no image is displayed in the viewer. Seth and I had a brief chat in the islandora8 slack channel, ultimately he implemented a context (below) that basically removes openseadragon when a media cannot be viewed by anonymous. This serves as a stop-gap so that logged in users viewing restricted materials do not see "ugly" cantaloupe with a big 403 in it. To get this actually functional, we'd need to get Cantaloupe to understand the permissions. Seth pointed to https://cantaloupe-project.github.io/manual/5.0/access-control.html#IIIF%20Authentication%20API and https://openseadragon.github.io/docs/OpenSeadragon.IIIFTileSource.html#configure as potential resources to get started on that work but it is a significant undertaking. The other option would be to open up openseadragon by permissions to all items but that is an obvious and fatal security hole.

<?php
namespace Drupal\islandora_local\Plugin\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
 * Condition to detect if is a node is referenced by a configured field.
 *
 * @Condition(
 *   id = "node_anon_access",
 *   label = @Translation("Node accessible by Anonymous"),
 *   context = {
 *     "node" = @ContextDefinition("entity:node", required = TRUE , label = @Translation("node"))
 *   }
 * )
 */
class NodeAnonAccess extends ConditionPluginBase implements ContainerFactoryPluginInterface {
  /**
   * Node storage.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;
  /**
   * Constructor.
   *
   * @param array $configuration
   *   The plugin configuration, i.e. an array with configuration values keyed
   *   by configuration option name. The special key 'context' may be used to
   *   initialize the defined contexts by setting it to an array of context
   *   values keyed by context names.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
   *   Entity type manager.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    EntityTypeManager $entity_type_manager
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->entityTypeManager = $entity_type_manager;
  }
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager')
    );
  }
  /**
   * {@inheritdoc}
   */
  public function evaluate() {
    $node = $this->getContextValue('node');
    $annon_has_access = $node->access('view', User::getAnonymousUser());
    return $annon_has_access;
  }
  /**
   * {@inheritdoc}
   */
  public function summary() {
    if (!empty($this->configuration['negate'])) {
      return $this->t('The node is not accessible by Anonymous.');
    }
    else {
      return $this->t('The node is accessible by Anonymous.');
    }
  }
}
elizoller commented 4 years ago

DGI says they are using a custom delegates.rb with a token passed from Drupal in the header But it means you're basically on the hook for enforcing all of the permissions ruby-side

jonathangreen commented 4 years ago

I don't have an example of this in action, but I think a token could be passed to Cantaloupe without having to enforce permissions on the cantaloupe side. The flow of traffic would look something like this:

Seadragon (js, in browser) -> Cantaloupe -> Drupal  

At LYRASIS we haven't tackled this for Islandora 8, but we do something like this with Cantaloupe in our Islandora 7 install. ISLE 7 uses a delegates.rb to do something like this as well.

The trick is to authenticate the connection between Cantaloupe and Drupal. That is where delegates.rb comes in. Since the delegates script has access to the incoming request, it can add a token that was send in a header in the incoming request from Seadragon to the outgoing request to Drupal.

This token could be a JWT like we are sending to the backend microservices. It could also be something like the Drupal session. Either way the request from Cantaloupe will end up looking like its coming from the current user, so Drupal can enforce the access restrictions like usual.