Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port digital-utsc work to pass auth token to Cantaloupe. #32

Draft
wants to merge 5 commits into
base: 2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions islandora_mirador.module
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ function islandora_mirador_theme() {
function template_preprocess_mirador(&$variables) {
$variables['mirador_view_id'] = Html::getUniqueId($variables['mirador_view_id']);

if (!empty(\Drupal::hasService('jwt.authentication.jwt'))) {
$variables['#attached']['drupalSettings']['token'] = \Drupal::service('jwt.authentication.jwt')->generateToken();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This token's TTL needs to be part of the cacheable metadata, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much.

}
Comment on lines +38 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This continues to propagate the issue of time-limited tokens being maintained potentially perpetually in cache: needs the cache metadata to limit how long the templated content could be used.



/**
* @var \Drupal\islandora_mirador\IslandoraMiradorPluginManager
*/
Expand Down
6 changes: 6 additions & 0 deletions islandora_mirador.routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ islandora_mirador.miradorconfig:
_title: 'Mirador Settings'
requirements:
_permission: 'administer site configuration'
islandora_mirador.service_worker:
path: '/islandora_mirador_service_worker'
defaults:
_controller: '\Drupal\islandora_mirador\Controller\ServiceWorkerController::serve'
requirements:
_permission: 'access content'
49 changes: 48 additions & 1 deletion js/mirador_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,57 @@
attach: function (context, settings) {
Object.entries(settings.mirador.viewers).forEach(entry => {
const [base, values] = entry;
once('mirador-viewer', base, context).forEach(() =>
once('mirador-viewer', base, context, settings).forEach(() => {
if (settings.token !== undefined) {
values["resourceHeaders"] = {
'Authorization': 'Bearer '+ settings.token,
'token': settings.token
};
values["requestPipeline"] = [
(url, options) => ({ ...options, headers: {
"Accept": 'application/ld+json;profile="http://iiif.io/api/presentation/3/context.json"',
'Authorization': 'Bearer '+ settings.token,
'token': settings.token
}})
];
values["osdConfig"] = {
"loadTilesWithAjax": true,
"ajaxHeaders": {
'Authorization': 'Bearer '+ settings.token,
'token': settings.token
}
};
values["requests"] = {
preprocessors: [ // Functions that receive HTTP requests and manipulate them (e.g. to add headers)
// rewrite all info.json requests to add the text/json request header
(url, options) => (url.match('info.json') && { ...options, headers: {
'Authorization': 'Bearer '+ settings.token,
'token': settings.token
}})
],
};

}
Mirador.viewer(values, window.miradorPlugins || {})
}
);
});
if (settings.token !== undefined) {
if ('serviceWorker' in navigator) {
// The Mirador viewer uses img tags for thumbnails so thumbnail image requests
// do not have authorization or token headers. Attach them using a service worker.
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/islandora_mirador_service_worker?token=' + settings.token, { scope: '/' })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is fully portable, with the assumption of the site being directly on the root? As in, if using language or site path prefixing, then should this follow suit? Drupal might expose a helper for this?

.then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(err => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
}
},
detach: function (context, settings) {
Object.entries(settings.mirador.viewers).forEach(entry => {
Expand Down
21 changes: 21 additions & 0 deletions js/service_worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
self.addEventListener('activate', function (event) {
console.log('Service Worker: claiming control...');
return self.clients.claim();
});

self.addEventListener('fetch', function (event) {
if (event.request.destination === "image" && new URL(event.request.url).pathname.startsWith('/cantaloupe/iiif/') && new URL(location).searchParams.has('token')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure the .startsWith('/cantaloupe/iiif') bit here is properly portable? Is into deployment details?

console.log('Service Worker: fetching...');
var token = new URL(location).searchParams.get('token');
event.respondWith(
fetch(event.request, {
headers: {
'Authorization': 'Bearer ' + token,
'token': token
},
mode: "cors",
credentials: "include"
})
);
}
});
249 changes: 249 additions & 0 deletions scripts/delegates.rb
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to go in the cantaloupe server?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; although I'm sure it could be slimmed down.

Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
##
# Sample Ruby delegate script containing stubs and documentation for all
# available delegate methods. See the user manual for more information.
#
# The application will create an instance of this class early in the request
# cycle and dispose of it at the end of the request cycle. Instances don't need
# to be thread-safe, but sharing information across instances (requests)
# **does** need to be done thread-safely.
#
# This version of the script works with Cantaloupe version 4, and not earlier
# versions. Likewise, earlier versions of the script are not compatible with
# Cantaloupe 4.
#
class CustomDelegate

##
# Attribute for the request context, which is a hash containing information
# about the current request.
#
# This attribute will be set by the server before any other methods are
# called. Methods can access its keys like:
#
# ```
# identifier = context['identifier']
# ```
#
# The hash will contain the following keys in response to all requests:
#
# * `client_ip` [String] Client IP address.
# * `cookies` [Hash<String,String>] Hash of cookie name-value pairs.
# * `identifier` [String] Image identifier.
# * `request_headers` [Hash<String,String>] Hash of header name-value pairs.
# * `request_uri` [String] Public request URI.
# * `scale_constraint` [Array<Integer>] Two-element array with scale
# constraint numerator at position 0 and denominator at
# position 1.
#
# It will contain the following additional string keys in response to image
# requests:
#
# * `full_size` [Hash<String,Integer>] Hash with `width` and `height`
# keys corresponding to the pixel dimensions of the
# source image.
# * `operations` [Array<Hash<String,Object>>] Array of operations in
# order of application. Only operations that are not
# no-ops will be included. Every hash contains a `class`
# key corresponding to the operation class name, which
# will be one of the `e.i.l.c.operation.Operation`
# implementations.
# * `output_format` [String] Output format media (MIME) type.
# * `resulting_size` [Hash<String,Integer>] Hash with `width` and `height`
# keys corresponding to the pixel dimensions of the
# resulting image after all operations have been applied.
#
# @return [Hash] Request context.
#
attr_accessor :context

##
# Returns authorization status for the current request. Will be called upon
# all requests to all public endpoints.
#
# Implementations should assume that the underlying resource is available,
# and not try to check for it.
#
# Possible return values:
#
# 1. Boolean true/false, indicating whether the request is fully authorized
# or not. If false, the client will receive a 403 Forbidden response.
# 2. Hash with a `status_code` key.
# a. If it corresponds to an integer from 200-299, the request is
# authorized.
# b. If it corresponds to an integer from 300-399:
# i. If the hash also contains a `location` key corresponding to a
# URI string, the request will be redirected to that URI using
# that code.
# ii. If the hash also contains `scale_numerator` and
# `scale_denominator` keys, the request will be
# redirected using that code to a virtual reduced-scale version of
# the source image.
# c. If it corresponds to 401, the hash must include a `challenge` key
# corresponding to a WWW-Authenticate header value.
#
# @param options [Hash] Empty hash.
# @return [Boolean,Hash<String,Object>] See above.
#
def authorize(options = {})
true
end

##
# Used to add additional keys to an information JSON response. See the
# [Image API specification](http://iiif.io/api/image/2.1/#image-information).
#
# @param options [Hash] Empty hash.
# @return [Hash] Hash that will be merged into an IIIF Image API 2.x
# information response. Return an empty hash to add nothing.
#
def extra_iiif2_information_response_keys(options = {})
=begin
Example:
{
'attribution' => 'Copyright My Great Organization. All rights '\
'reserved.',
'license' => 'http://example.org/license.html',
'logo' => 'http://example.org/logo.png',
'service' => {
'@context' => 'http://iiif.io/api/annex/services/physdim/1/context.json',
'profile' => 'http://iiif.io/api/annex/services/physdim',
'physicalScale' => 0.0025,
'physicalUnits' => 'in'
}
}
=end
{}
end

##
# Tells the server which source to use for the given identifier.
#
# @param options [Hash] Empty hash.
# @return [String] Source name.
#
def source(options = {})
end

##
# N.B.: this method should not try to perform authorization. `authorize()`
# should be used instead.
#
# @param options [Hash] Empty hash.
# @return [String,nil] Blob key of the image corresponding to the given
# identifier, or nil if not found.
#
def azurestoragesource_blob_key(options = {})
end

##
# N.B.: this method should not try to perform authorization. `authorize()`
# should be used instead.
#
# @param options [Hash] Empty hash.
# @return [String,nil] Absolute pathname of the image corresponding to the
# given identifier, or nil if not found.
#
def filesystemsource_pathname(options = {})
end

##
# Returns one of the following:
#
# 1. String URI
# 2. Hash with the following keys:
# * `uri` [String] (required)
# * `username` [String] For HTTP Basic authentication (optional).
# * `secret` [String] For HTTP Basic authentication (optional).
# * `headers` [Hash<String,String>] Hash of request headers (optional).
# 3. nil if not found.
#
# N.B.: this method should not try to perform authorization. `authorize()`
# should be used instead.
#
# @param options [Hash] Empty hash.
# @return See above.
#
def httpsource_resource_info(options = {})
return { "uri" => context['identifier'], "headers" => { "Authorization" => context['request_headers']['authorization'] } }
end

##
# N.B.: this method should not try to perform authorization. `authorize()`
# should be used instead.
#
# @param options [Hash] Empty hash.
# @return [String] Identifier of the image corresponding to the given
# identifier in the database.
#
def jdbcsource_database_identifier(options = {})
end

##
# Returns either the media (MIME) type of an image, or an SQL statement that
# can be used to retrieve it, if it is stored in the database. In the latter
# case, the "SELECT" and "FROM" clauses should be in uppercase in order to
# be autodetected. If nil is returned, the media type will be inferred some
# other way, such as by identifier extension or magic bytes.
#
# @param options [Hash] Empty hash.
# @return [String, nil]
#
def jdbcsource_media_type(options = {})
end

##
# @param options [Hash] Empty hash.
# @return [String] SQL statement that selects the BLOB corresponding to the
# value returned by `jdbcsource_database_identifier()`.
#
def jdbcsource_lookup_sql(options = {})
end

##
# N.B.: this method should not try to perform authorization. `authorize()`
# should be used instead.
#
# @param options [Hash] Empty hash.
# @return [Hash<String,Object>,nil] Hash containing `bucket` and `key` keys;
# or nil if not found.
#
def s3source_object_info(options = {})
end

##
# Tells the server what overlay, if any, to apply to an image in response
# to a request. Will be called upon all image requests to any endpoint if
# overlays are enabled and the overlay strategy is set to `ScriptStrategy`
# in the application configuration.
#
# N.B.: When a string overlay is too large or long to fit entirely within
# the image, it won't be drawn. Consider breaking long strings with LFs (\n).
#
# @param options [Hash] Empty hash.
# @return [Hash<String,String>,nil] For image overlays, a hash with `image`,
# `position`, and `inset` keys. For string overlays, a hash with
# `background_color`, `color`, `font`, `font_min_size`, `font_size`,
# `font_weight`, `glyph_spacing`,`inset`, `position`, `string`,
# `stroke_color`, and `stroke_width` keys.
# Return nil for no overlay.
#
def overlay(options = {})
puts "overlay"
end

##
# Tells the server what regions of an image to redact in response to a
# particular request. Will be called upon all image requests to any endpoint
# if redactions are enabled in the application configuration.
#
# @param options [Hash] Empty hash.
# @return [Array<Hash<String,Integer>>] Array of hashes, each with `x`, `y`,
# `width`, and `height` keys; or an empty array if no redactions are
# to be applied.
#
def redactions(options = {})
puts "redactions"
[]
end

end
Loading