Skip to content

Instantly share code, notes, and snippets.

@jeremyf
Created January 23, 2024 21:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremyf/0d80aae5794be36b6505db720bcd0946 to your computer and use it in GitHub Desktop.
Save jeremyf/0d80aae5794be36b6505db720bcd0946 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
module Hyrax
##
# @api public
#
# Cast an object to its AccessControlList
#
# @param [Object] an object to try to cast
#
# @return [Hyrax::AccessControlList]
def self.AccessControlList(obj)
case obj
when AccessControlList
obj
else
obj.try(:permission_manager)&.acl || AccessControlList.new(resource: obj)
end
end
##
# @api public
#
# ACLs for `Hyrax::Resource` models
#
# Allows managing `Hyrax::Permission` entries referring to a specific
# `Hyrax::Resource` using a simple add/delete model.
#
# @example Using the Grant DSL
# my_resource = Hyrax.query_service.find_by(id: 'my_id')
#
# acl = Hyrax::AccessControlList.new(resource: resource)
# acl.permissions # => #<Set: {}>
#
# user = User.first
# group = Group.new('public')
#
# acl.grant(:read).to(group)
# acl.grant(:edit).to(user)
#
# acl.permissions
# # => #<Set: {#<Hyrax::Permission access_to=#<Valkyrie::ID:0x000055628b0ae0b8 @id="my_id"> agent="group/public" mode=:read>,
# #<Hyrax::Permission access_to=#<Valkyrie::ID:0x000055628be41388 @id="my_id"> agent="user1@example.com" mode=:edit>}>
#
# acl.revoke(:edit).from(user)
#
# acl.permissions
# # => #<Set: {#<Hyrax::Permission access_to=#<Valkyrie::ID:0x000055628b0ae0b8 @id="my_id"> agent="group/public" mode=:read>}>
class AccessControlList
##
# @!attribute [r] resource
# @return [Valkyrie::Resource]
# @!attribute [r] persister
# @return [#save]
# @!attribute [r] query_service
# @return [#find_inverse_references_by]
attr_reader :persister, :query_service
attr_reader :resource
##
# @param value [Valkyrie::Resource]
# @return [Valkyrie::Resource]
def resource=(value)
@resource = value
# We need to remove the memoization as the @resource has changed.
@change_set = nil
@access_control_model = nil
@resource
end
##
# @api public
#
# @param resource [Valkyrie::Resource]
# @param persister [#save] defaults to the configured Hyrax persister
# @param query_service [#find_inverse_references_by] defaults to the
# configured Hyrax query service
def initialize(resource:, persister: Hyrax.persister, query_service: Hyrax.query_service)
self.resource = resource
@persister = persister
@query_service = query_service
end
##
# Copy and save permissions from source to target
#
# @param [Valkyrie::Resource, Hyrax::AccessControlList] source
# @param [Valkyrie::Resource, Hyrax::AccessControlList] target
#
# @return [Hyrax::AccessControlList] an acl for `target` with the updated permissions
def self.copy_permissions(source:, target:)
target = Hyrax::AccessControlList(target)
target.permissions = Hyrax::AccessControlList(source).permissions
target.save && target
end
##
# @api public
#
# @param permission [Hyrax::Permission]
#
# @return [Boolean]
def <<(permission)
permission.access_to = resource.id
change_set.permissions += [permission]
true
end
alias add <<
##
# @api public
#
# @param permission [Hyrax::Permission]
#
# @return [Boolean]
def delete(permission)
change_set.permissions -= [permission]
true
end
##
# @api public
#
# @example
# user = User.find('user_id')
#
# acl.grant(:read).to(user)
def grant(mode)
ModeGrant.new(self, mode)
end
##
# @api public
#
# @return [Boolean]
def pending_changes?
change_set.changed?
end
##
# @api public
#
# @return [Set<Hyrax::Permission>]
def permissions
require 'byebug'; byebug if $HELLO
Set.new(change_set.permissions)
end
##
# @api public
#
# @return [Array<Hyrax::Permission>]
def permissions=(new_permissions)
change_set.permissions = []
new_permissions.each { |p| self << p }
end
##
# @api public
#
# @example
# user = User.find('user_id')
#
# acl.revoke(:read).from(user)
def revoke(mode)
ModeRevoke.new(self, mode)
end
##
# @api public
#
# Saves the ACL for the resource, by saving each permission policy
#
# @return [Boolean]
def save
return true unless pending_changes?
change_set.sync
# change_set.resource is a Hyrax::AccessControl
#
# We're setting the once fetched access_control_model to what was returned, so as to avoid
# a refetch
@access_control_model = persister.save(resource: change_set.resource)
# self.resource is a Hyrax::Resource
Hyrax.publisher.publish('object.acl.updated', acl: self, result: :success)
@change_set = nil
true
end
##
# @api public
#
# Deletes the ACL for the resource
#
# @return [Boolean]
def destroy
persister.delete(resource: change_set.resource) if change_set.resource.persisted?
@change_set = nil
true
end
private
##
# @abstract
# @api private
class ModeEditor
def initialize(acl, mode)
@acl = acl
@mode = mode.to_sym
end
private
##
# Returns the identifier used by ACLs to identify agents.
#
# This defaults to the `:agent_key`, but if that method doesn’t exist,
# `:user_key` will be used as a fallback.
def id_for(agent:)
key = agent.try(:agent_key) || agent.user_key
key.to_s
end
end
##
# @api private
#
# A short-term memory object for the permission granting DSL. Use with
# method chaining, as in: `acl.grant(:edit).to(user)`.
class ModeGrant < ModeEditor
##
# @api public
# @return [Hyrax::AccessControlList]
def to(user_or_group)
agent_id = id_for(agent: user_or_group)
@acl << Hyrax::Permission.new(access_to: @acl.resource.id, agent: agent_id, mode: @mode)
@acl
end
end
##
# @api private
#
# A short-term memory object for the permission revoking DSL. Use with
# method chaining, as in: `acl.revoke(:edit).from(user)`.
class ModeRevoke < ModeEditor
##
# @api public
# @return [Hyrax::AccessControlList]
def from(user_or_group)
permission_for_deletion = @acl.permissions.find do |p|
p.mode == @mode &&
p.agent.to_s == id_for(agent: user_or_group)
end
@acl.delete(permission_for_deletion) if permission_for_deletion
@acl
end
end
##
# @api private
def access_control_model
@access_control_model ||= AccessControl.for(resource: resource, query_service: query_service)
end
##
# @api private
def change_set
@change_set ||= Hyrax::ChangeSet.for(access_control_model)
end
end
end
it do
adapter = Valkyrie::MetadataAdapter.find(:postgres_adapter)
query_service = adapter.query_service
persister = adapter.persister
allow(Hyrax).to receive(:query_service).and_return(query_service)
allow(Hyrax).to receive(:persister).and_return(persister)
# Save the resource in the metadata adapter.
resource = adapter.persister.save(resource: build(:hyrax_resource))
acl = Hyrax::AccessControlList.new(resource:, persister:, query_service:)
# Conjecture is this will work, then removing the changes in the described_class will then
# fail.
#
# Guidance for testing, such as acceptance criteria or new user interface behaviors:
# create a record in the console
acl.save
# indexer = Hyrax::Indexers::ResourceIndexer(resource)
indexing_adapter = Valkyrie::IndexingAdapter.find(:solr_index)
# index it
indexing_adapter.save(resource: resource)
# view the visibility and permisisons
# is this querying Solr?
# We want to query Solr...this is probably querying Postgres
resource = adapter.query_service.find_by(id: resource.id)
expect(resource.visibility).to eq('restricted')
expect(resource.permission_manager.acl.permissions).to be_empty
# This is a person read permission
permission = FactoryBot.build(:permission)
acl = Hyrax::AccessControlList.new(resource:, persister: ,query_service:)
# save the acl
acl << permission
acl.save
expect(acl.permissions).not_to be_empty
indexing_adapter.save(resource: acl.resource)
# view the visibility and permissions
resource = adapter.query_service.find_by(id: resource.id)
expect(resource.permission_manager.acl.permissions).not_to be_empty
# index it again
indexing_adapter.save(resource: resource)
# Note: There's no read_people; just edit_people, edit_groups, read_groups
doc = ::SolrDocument.new(Hyrax::SolrService.search_by_id(resource.id))
# view the visibility and permisisons
resource = adapter.query_service.find_by(id: resource.id)
expect(resource.permission_manager.acl.permissions).not_to be_empty
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment