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

[WIP] Create an API for asking about access to a file #196

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions app/controllers/v1/access_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module V1
# Answers the question. Can the given agent view the given resource.
# e.g. GET /v1/authorize/:level/:druid/:file_name?agent[user_key]=jcoyne85&agent[stanford]=true
# Where ':level' is 'read', 'download', or 'access'
class AccessController < ApplicationController
def show
ident = ResourceIdentifier.new(druid: params[:druid], file_name: params[:file_name])
agent = Agent.new
access = AccessService.new(identifier: ident, level: params[:level], agent: agent)
render json: { authorized: access.authorized? }
end
end
end
20 changes: 20 additions & 0 deletions app/models/agent.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Information about a user including their user_key,
# stanford affiliation and location
class Agent
def initialize(options = {})
@options = options.slice(:user_key, :stanford, :location)
end

# @return [String] an IP address
def location
@options[:location]
end

def stanford?
@options[:stanford]
end

def user_key
@options[:user_key]
end
end
8 changes: 8 additions & 0 deletions app/models/resource_identifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ResourceIdentifier
def initialize(druid:, file_name:)
@druid = druid
@file_name = file_name
end

attr_reader :druid, :file_name
end
140 changes: 140 additions & 0 deletions app/services/access_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
class AccessService
# @param level [String] What level of access is being requested
# @param agent [Agent] Who is making the request
# @param identifier [ResourceIdentifier] What resource is being requested
def initialize(level:, agent:, identifier:)
@level = level
@agent = agent
@identifier = identifier
end

def authorized?
case @level
when 'read'
readable_by?(@agent)
when 'access'
accessable_by?(@agent)
when 'download'
readable_by?(@agent) # We may need to add more here about projection size?
end
end

private

def id
@identifier
end

def readable_by?(user)
world_downloadable? ||
(stanford_only_downloadable? && user.stanford?) ||
# (agent_downloadable?(user.user_key) && user.app_user?) ||
location_downloadable?(user.location)
end

def accessable_by?(user)
world_accessable? ||
(stanford_only_accessable? && user.stanford?) ||
agent_accessable?(user) ||
location_accessable?(user.location)
end

def maybe_downloadable?
world_unrestricted? || stanford_only_unrestricted?
end

def stanford_restricted?
stanford_only_rights.first
end

# Returns true if a given file has any location restrictions.
# Falls back to the object-level behavior if none at file level.
def restricted_by_location?
rights.restricted_by_location?(id.file_name)
end

# Returns [<Boolean>, <String>]: whether a file-level group/stanford node exists, and the value of its rule attribute
# If a group/stanford node does not exist for this file, then object-level group/stanford rights are returned
def stanford_only_rights
rights.stanford_only_rights_for_file id.file_name
end

# Returns [<Boolean>, <String>]: whether a file-level location exists, and the value of its rule attribute
# If a location node does not exist for this file, then object-level location rights are returned
def location_rights(location)
rights.location_rights_for_file(id.file_name, location)
end

def world_accessable?
world_rights.first
end

def agent_accessable?(user)
agent_rights_defined = agent_rights(user.user_key).first
agent_rights_defined && user.app_user?
end

def location_accessable?(location)
location_rights(location).first
end

def world_unrestricted?
rights.world_unrestricted_file? id.file_name
end

def world_downloadable?
rights.world_downloadable_file? id.file_name
end

# Returns [<Boolean>, <String>]: whether a file-level world node exists, and the value of its rule attribute
# If a world node does not exist for this file, then object-level world rights are returned
def world_rights
rights.world_rights_for_file id.file_name
end

def stanford_only_downloadable?
rights.stanford_only_downloadable_file? id.file_name
end

def stanford_only_accessable?
stanford_only_rights.first
end

# Returns true if the file is stanford-only readable AND has no rule attribute
# If a stanford node does not exist for this file, then object-level stanford rights are returned
def stanford_only_unrestricted?
rights.stanford_only_unrestricted_file? id.file_name
end

# Returns [<Boolean>, <String>]: whether a file-level agent node exists, and the value of its rule attribute
# If an agent node does not exist for this file, then object-level agent rights are returned
def agent_rights(agent)
rights.agent_rights_for_file id.file_name, agent
end

def agent_downloadable?(agent)
value, rule = agent_rights(agent)
value && (rule.nil? || rule != Dor::RightsAuth::NO_DOWNLOAD_RULE)
end

def restricted_locations
if rights.file[file_name]
rights.file[file_name].location.keys
else
rights.obj_lvl.location.keys
end
end

def location_downloadable?(location)
value, rule = location_rights(location)
value && (rule.nil? || rule != Dor::RightsAuth::NO_DOWNLOAD_RULE)
end

def rights
@rights ||= resource.rights.rights_auth
end

def resource
@resource ||= PurlResource.find(@identifier.druid)
end
end
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@
get '/:id/iiif/annotation/:annotation_id' => 'iiif_v2#annotation', as: :iiif_annotation, format: false
get '/:id/iiif/annotation/:annotation_id.json', to: redirect('/%{id}/iiif/annotation/%{annotation_id}')

namespace 'v1' do
get '/authorize/:level/:druid/:file_name' => 'access#show'
end
end
20 changes: 20 additions & 0 deletions spec/controller/v1/access_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'rails_helper'

RSpec.describe V1::AccessController, type: :controller do
describe 'show' do
let(:service) { instance_double(AccessService, authorized?: true) }

before do
allow(AccessService).to receive(:new)
.with(identifier: ResourceIdentifier, level: 'read', agent: Agent)
.and_return(service)
end

it 'returns the status' do
get :show, params: { level: 'read', druid: '12348', file_name: 'bleh.jp2' }
json = JSON.parse(response.body)
expect(response).to be_successful
expect(json).to eq('authorized' => true)
end
end
end
151 changes: 151 additions & 0 deletions spec/services/access_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
require 'rails_helper'

RSpec.describe AccessService do
describe 'authorized?' do
subject { instance.authorized? }
let(:instance) { AccessService.new(level: level, agent: agent, identifier: identifier) }

it 'enforces location based access'

context 'when requesting read' do
let(:level) { 'read' }

context 'as an unidentified user' do
let(:agent) { Agent.new }

context 'on a public object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bb157hs6068', file_name: 'bb157hs6068_05_0001') }
it { is_expected.to be true }
end

context 'on a no-download object' do
let(:identifier) { ResourceIdentifier.new(druid: 'tx027jv4938', file_name: '2012-015GHEW-BW-1984-b4_1.4_0003') }
it { is_expected.to be false }
end

context 'on a stanford-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'zk091xr3370', file_name: 'Wei Huang_PhD Dissertation_Bioengineering_Jan 2012-augmented') }
it { is_expected.to be false }
end

context 'on a citation-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bc421tk1152', file_name: 'bc421tk1152_00_0001') }
it { is_expected.to be false }
end
end

context 'as a stanford user' do
let(:agent) { Agent.new(stanford: true) }

context 'on a stanford-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'zk091xr3370', file_name: 'Wei Huang_PhD Dissertation_Bioengineering_Jan 2012-augmented') }
it { is_expected.to be true }
end

context 'on a no-download object' do
let(:identifier) { ResourceIdentifier.new(druid: 'tx027jv4938', file_name: '2012-015GHEW-BW-1984-b4_1.4_0003') }
it { is_expected.to be true }
end

context 'on a citation-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bc421tk1152', file_name: 'bc421tk1152_00_0001') }
it { is_expected.to be false }
end
end
end

context 'when requesting download' do
let(:level) { 'download' }

context 'as an unidentified user' do
let(:agent) { Agent.new }

context 'on a public object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bb157hs6068', file_name: 'bb157hs6068_05_0001') }
it { is_expected.to be true }
end

context 'on a no-download object' do
let(:identifier) { ResourceIdentifier.new(druid: 'tx027jv4938', file_name: '2012-015GHEW-BW-1984-b4_1.4_0003') }
it { is_expected.to be false }
end

context 'on a stanford-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'zk091xr3370', file_name: 'Wei Huang_PhD Dissertation_Bioengineering_Jan 2012-augmented') }
it { is_expected.to be false }
end

context 'on a citation-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bc421tk1152', file_name: 'bc421tk1152_00_0001') }
it { is_expected.to be false }
end
end

context 'as a stanford user' do
let(:agent) { Agent.new(stanford: true) }

context 'on a stanford-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'zk091xr3370', file_name: 'Wei Huang_PhD Dissertation_Bioengineering_Jan 2012-augmented') }
it { is_expected.to be true }
end

context 'on a no-download object' do
let(:identifier) { ResourceIdentifier.new(druid: 'tx027jv4938', file_name: '2012-015GHEW-BW-1984-b4_1.4_0003') }
it { is_expected.to be true }
end

context 'on a citation-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bc421tk1152', file_name: 'bc421tk1152_00_0001') }
it { is_expected.to be false }
end
end
end

context 'when requesting access' do
let(:level) { 'access' }

context 'as an unidentified user' do
let(:agent) { Agent.new }

context 'on a public object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bb157hs6068', file_name: 'bb157hs6068_05_0001') }
it { is_expected.to be true }
end

context 'on a no-download object' do
let(:identifier) { ResourceIdentifier.new(druid: 'tx027jv4938', file_name: '2012-015GHEW-BW-1984-b4_1.4_0003') }
it { is_expected.to be true }
end

context 'on a stanford-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'zk091xr3370', file_name: 'Wei Huang_PhD Dissertation_Bioengineering_Jan 2012-augmented') }
it { is_expected.to be false }
end

context 'on a citation-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bc421tk1152', file_name: 'bc421tk1152_00_0001') }
it { is_expected.to be false }
end
end

context 'as a stanford user' do
let(:agent) { Agent.new(stanford: true) }

context 'on a stanford-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'zk091xr3370', file_name: 'Wei Huang_PhD Dissertation_Bioengineering_Jan 2012-augmented') }
it { is_expected.to be true }
end

context 'on a no-download object' do
let(:identifier) { ResourceIdentifier.new(druid: 'tx027jv4938', file_name: '2012-015GHEW-BW-1984-b4_1.4_0003') }
it { is_expected.to be true }
end

context 'on a citation-only object' do
let(:identifier) { ResourceIdentifier.new(druid: 'bc421tk1152', file_name: 'bc421tk1152_00_0001') }
it { is_expected.to be false }
end
end
end
end
end