-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🎁 Adding WorkAuthorization model for CDL
With this commit, we're adding a mechanism to "lend" folks books. This change accounts for granting a user read rights to a work. It does not introduce the logic/functionality that "authorizes" or "revokes" this loaned resource. With this commit, there are no changed production logic paths. Related to: - #633 Co-authored-by: Summer Cook <[email protected]>
- Loading branch information
1 parent
a9a6f2f
commit 6ae6d6b
Showing
4 changed files
with
161 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# frozen_string_literal: true | ||
|
||
# WorkAuthorization models users granted access to works. The instigator of the authorizations is | ||
# outside the model. In the case of PALNI/PALCI there will be a Shibboleth/SAML authentication that | ||
# indicates we should create a WorkAuthorization entry. | ||
# | ||
# @note Transactions across data storage layers (e.g. postgres and fedora) are precarious. Fedora | ||
# doesn't have proper transactions and there is not a clear concept of Postgres and Fedora | ||
# sharing a transaction pool. However, we can emulate one by having a postgres transaction that: | ||
# first does all of the postgres and then does one (ideally single) fedora change. It is | ||
# not bullet proof but does hopefully improve the chances of data integrity. | ||
# | ||
# @see https://github.com/scientist-softserv/palni-palci/issues/633 | ||
class WorkAuthorization < ActiveRecord::Base # rubocop:disable ApplicationRecord | ||
class WorkNotFoundError < StandardError | ||
def initialize(user:, work_pid:) | ||
"Unable to authorize #{user.class} #{user.user_key.inspect} for work with ID=#{work_pid} because work does not exist." | ||
end | ||
end | ||
|
||
belongs_to :user | ||
|
||
# This will be a non-ActiveRecord resource | ||
validates :work_pid, presence: true | ||
|
||
## | ||
# Grant the given :user permission to read the work associated with the given :work_pid. | ||
# | ||
# @param user [User] | ||
# @param work_pid [String] | ||
# | ||
# @raise [WorkAuthorization::WorkNotFoundError] when the given :work_pid is not found. | ||
# | ||
# @see .revoke! | ||
# rubocop:disable Rails/FindBy | ||
def self.authorize!(user:, work_pid:) | ||
work = ActiveFedora::Base.where(id: work_pid).first | ||
raise WorkNotFoundError.new(user: user, work_pid: work_pid) unless work | ||
|
||
transaction do | ||
authorization = find_or_create_by!(user_id: user.id, work_pid: work.id) | ||
authorization.update!(work_title: work.title) | ||
work.set_read_users([user.user_key], [user.user_key]) | ||
work.save! | ||
end | ||
end | ||
# rubocop:enable Rails/FindBy | ||
|
||
## | ||
# Remove permission for the given :user to read the work associated with the given :work_pid. | ||
# | ||
# @param user [User] | ||
# @param work_pid [String] | ||
# | ||
# @see .authorize! | ||
# rubocop:disable Rails/FindBy | ||
def self.revoke!(user:, work_pid:) | ||
# When we delete the authorizations, we want to ensure that we've tidied up the corresponding | ||
# work's read users. If for some reason the ActiveFedora save fails, this the destruction of | ||
# the authorizations will rollback. Meaning we still have a record of what we've authorized. | ||
transaction do | ||
where(user_id: user.id, work_pid: work_pid).destroy_all | ||
work = ActiveFedora::Base.where(id: work_pid).first | ||
if work | ||
work.set_read_users([], [user.user_key]) | ||
work.save! | ||
end | ||
true | ||
end | ||
end | ||
# rubocop:enable Rails/FindBy | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
class AddWorkAuthorizations < ActiveRecord::Migration[5.2] | ||
def change | ||
create_table "work_authorizations", force: :cascade do |t| | ||
t.string "work_title" | ||
t.belongs_to "user" | ||
t.string "work_pid", index: true | ||
t.string "scope" | ||
t.string "error", default: nil | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
require 'spec_helper' | ||
|
||
require 'cancan/matchers' | ||
|
||
RSpec.describe WorkAuthorization, type: :model do | ||
let(:work) { FactoryBot.create(:generic_work) } | ||
let(:borrowing_user) { FactoryBot.create(:user) } | ||
let(:ability) { ::Ability.new(borrowing_user) } | ||
|
||
describe '.authorize!' do | ||
it 'gives the borrowing user the ability to "read" the work' do | ||
# We re-instantiate an ability class because CanCan caches many of the ability checks. By | ||
# both passing the id and reinstantiating, we ensure that we have the most fresh data; that is | ||
# no cached ability "table" nor cached values on the work. | ||
expect { described_class.authorize!(user: borrowing_user, work_pid: work.id) } | ||
.to change { ::Ability.new(borrowing_user).can?(:read, work.id) }.from(false).to(true) | ||
end | ||
|
||
context 'when the work_pid is not found' do | ||
it 'raises a WorkAuthorization::WorkNotFoundError' do | ||
expect { described_class.authorize!(user: borrowing_user, work_pid: "oh-so-404") }.to raise_error(WorkAuthorization::WorkNotFoundError) | ||
end | ||
end | ||
end | ||
|
||
describe '.revoke!' do | ||
it 'revokes an authorized user from being able to "read" the work' do | ||
# Ensuring we're authorized | ||
described_class.authorize!(user: borrowing_user, work_pid: work.id) | ||
|
||
expect { described_class.revoke!(user: borrowing_user, work_pid: work.id) } | ||
.to change { ::Ability.new(borrowing_user).can?(:read, work.id) }.from(true).to(false) | ||
end | ||
|
||
it 'gracefully handles revocation of non-existent works' do | ||
expect(described_class.revoke!(user: borrowing_user, work_pid: "oh-so-404")).to be_truthy | ||
end | ||
|
||
it 'gracefully handles revoking that which was never authorized' do | ||
expect { described_class.revoke!(user: borrowing_user, work_pid: work.id) } | ||
.not_to change { ::Ability.new(borrowing_user).can?(:read, work.id) }.from(false) | ||
end | ||
end | ||
|
||
xdescribe 'adding errors' do | ||
let(:authorization) { WorkAuthorization.new } | ||
|
||
it 'adds the error' do | ||
authorization.update_error 'test error' | ||
expect(authorization.error).to eq('test error') | ||
end | ||
|
||
it 'clears error' do | ||
authorization.update_error 'test error' | ||
authorization.clear_error | ||
expect(authorization.error).to eq(nil) | ||
end | ||
end | ||
end |