From 83aa74d5b8f9262c72de79fc8f4fa27cebc2c0b8 Mon Sep 17 00:00:00 2001 From: Justin Coyne Date: Sat, 9 Dec 2017 21:47:10 -0600 Subject: [PATCH] [WIP] Create an API for asking about access to a file This would service stacks and provide a single API for this logic in our environment. --- app/controllers/v1/access_controller.rb | 13 ++ app/models/agent.rb | 20 +++ app/models/resource_identifier.rb | 8 + app/services/access_service.rb | 140 +++++++++++++++++ config/routes.rb | 3 + spec/controller/v1/access_controller_spec.rb | 20 +++ spec/services/access_service_spec.rb | 151 +++++++++++++++++++ 7 files changed, 355 insertions(+) create mode 100644 app/controllers/v1/access_controller.rb create mode 100644 app/models/agent.rb create mode 100644 app/models/resource_identifier.rb create mode 100644 app/services/access_service.rb create mode 100644 spec/controller/v1/access_controller_spec.rb create mode 100644 spec/services/access_service_spec.rb diff --git a/app/controllers/v1/access_controller.rb b/app/controllers/v1/access_controller.rb new file mode 100644 index 00000000..b057ddec --- /dev/null +++ b/app/controllers/v1/access_controller.rb @@ -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 diff --git a/app/models/agent.rb b/app/models/agent.rb new file mode 100644 index 00000000..68915ce6 --- /dev/null +++ b/app/models/agent.rb @@ -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 diff --git a/app/models/resource_identifier.rb b/app/models/resource_identifier.rb new file mode 100644 index 00000000..9046a6f4 --- /dev/null +++ b/app/models/resource_identifier.rb @@ -0,0 +1,8 @@ +class ResourceIdentifier + def initialize(druid:, file_name:) + @druid = druid + @file_name = file_name + end + + attr_reader :druid, :file_name +end diff --git a/app/services/access_service.rb b/app/services/access_service.rb new file mode 100644 index 00000000..b3176e66 --- /dev/null +++ b/app/services/access_service.rb @@ -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 [, ]: 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 [, ]: 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 [, ]: 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 [, ]: 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 diff --git a/config/routes.rb b/config/routes.rb index a2de84a8..73130c6f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/spec/controller/v1/access_controller_spec.rb b/spec/controller/v1/access_controller_spec.rb new file mode 100644 index 00000000..52787aee --- /dev/null +++ b/spec/controller/v1/access_controller_spec.rb @@ -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 diff --git a/spec/services/access_service_spec.rb b/spec/services/access_service_spec.rb new file mode 100644 index 00000000..8e51dd75 --- /dev/null +++ b/spec/services/access_service_spec.rb @@ -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