This version of the manual refers to an earlier version of the software.

Access Control

Authentication

To enable HTTP Basic authentication on image and information endpoints, set the following configuration keys:

endpoint.public.auth.basic.enabled = true
endpoint.public.auth.basic.username = myusername
endpoint.public.auth.basic.secret = mypassword

Authorization

A custom delegate method, authorized?(), can be used to implement authorization logic ranging from simple to complex. It will be invoked upon every image request. A skeleton with documented parameters and return values is present in the delegates.rb.sample file. By default, it just returns true, authorizing all requests.

Another method, redirect(), can be used to conditionally redirect to a different URL.

Notes

  • The authorization method will be called on every image request and should therefore be written to be efficient.
  • The authorization method will be called upon requests to all image endpoints, but not information endpoints.
  • Implementations should assume that the underlying source image exists, even though it may not. The image server will check for existence, but the check may occur after the delegate method is invoked.

Examples

Allow only requests for half-scale images or smaller

class CustomDelegate
  def authorized?(options = {})
    scale = context['operations'].find{ |op| op['class'] == 'Scale' }
    if scale
      max_scale = 0.5
      return scale['width'] <= full_size['width'] * max_scale and
          scale['height'] <= full_size['height'] * max_scale
    end
    false
  end
end

Allow only requests for identifiers matching a certain pattern

class CustomDelegate
  def authorized?(options = {})
    identifier = context['identifier']

    # Allow only identifiers that don't include "_restricted"
    return !identifier.include?('_restricted')
    # Allow only identifiers that start with "_public"
    return identifier.start_with?('public_')
    # Allow only identifiers matching a regex
    return identifier.match(/^image[5-9][0-9]/)
  end
end

Allow only requests for images set as "public" in a MySQL database

(The MySQL JDBC driver will need to be installed first.)

The arguments in the context hash are not sanitized. context['identifier'], for example, will be exactly as the application receives it. Prefer prepared statements over string concatenation in order to reduce vulnerability to injection attacks.
require 'rubygems'
require 'jdbc/mysql'
require 'java'

class CustomDelegate
  def authorized?(options = {})
    authorized = false

    Java::com.mysql.jdbc.Driver
    url = 'jdbc:mysql://HOST/DATABASE'
    conn = java.sql.DriverManager.get_connection(url, 'USERNAME', 'PASSWORD')
    stmt = conn.create_statement

    begin
      query = %q{SELECT is_public
          FROM image
          WHERE identifier = ?
          LIMIT 1}
      stmt = conn.prepare_statement(query)
      stmt.setString(1, context['identifier']);
      result_set = stmt.execute_query
      while result_set.next do
        authorized = result_set.getBoolean(1)
      end
    ensure
      stmt.close
      conn.close
    end
    authorized
  end
end

Allow only JPEG output

class CustomDelegate
  def authorized?(options = {})
    context['output_format'] == 'image/jpeg'
  end
end

Allow only certain user agents

This is not foolproof—if a client knows what User-Agent you are checking for, they can spoof it.

class CustomDelegate
  def authorized?(options = {})
    headers = context['request_headers']
    agent = headers.find{ |h, v| h.downcase == 'user-agent' }
    agent.start_with?('MyAllowedUserAgent/')
  end
end

Allow only requests that supply a valid token in a header

class CustomDelegate
  def authorized?(options = {})
    headers['X-MyToken'] == ... # write code to authorize the token
  end
end

Restrict a region of an image

In this example, requests for images containing any part of the bottom right quadrant of the source image will be denied.

(Also see redaction.)

class CustomDelegate
  def authorized?(options = {})
    crop = context['operations'].find{ |op| op['class'] == 'Crop' }
    if crop
      max_x = full_size['width'] / 2.0
      max_y = full_size['height'] / 2.0
      return !(crop['x'] + crop['width'] > max_x and
          crop['y'] + crop['height'] > max_y)
    end
    false
  end
end

Redirect to another URL

class CustomDelegate
  def redirect(options = {})
    {
      'location' => 'http://example.org/some-other-url',
      'status_code' => 302
    }
  end
end