Skip to content

Commit

Permalink
Merge pull request #53 from mdsol/develop
Browse files Browse the repository at this point in the history
adds bulk-getting permissions
  • Loading branch information
Connor Savage committed Feb 16, 2016
2 parents 4e81796 + 2a261cc commit 601bddd
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ and `gem 'neography'`. This should not be used in production since the interface

*ActiveRecord*

The ActiveRecord storage adapter talks to your existing MySQL database via your preconfigured
The ActiveRecord storage adapter talks to your existing MySQL or Postgres database via your preconfigured
ActiveRecord. You'll need to run `rails generate policy_machine migration` to add the necessary
tables to your database.

Expand Down
19 changes: 17 additions & 2 deletions lib/policy_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def add_association(user_attribute_pe, operation_set, object_attribute_pe)
# TODO: Parallelize the two component checks
def is_privilege?(user_or_attribute, operation, object_or_attribute, options = {})
is_privilege_ignoring_prohibitions?(user_or_attribute, operation, object_or_attribute, options) &&
!is_privilege_ignoring_prohibitions?(user_or_attribute, PM::Prohibition.on(operation), object_or_attribute, options)
(options[:ignore_prohibitions] || !is_privilege_ignoring_prohibitions?(user_or_attribute, PM::Prohibition.on(operation), object_or_attribute, options))
end

##
Expand Down Expand Up @@ -144,7 +144,7 @@ def is_privilege_ignoring_prohibitions?(user_or_attribute, operation, object_or_
end
end



##
# Returns an array of all privileges encoded in this
Expand Down Expand Up @@ -194,6 +194,21 @@ def scoped_privileges(user_or_attribute, object_or_attribute, options = {})
end
end

##
# Returns an array of all objects the given user (attribute)
# has the given operation on.
def accessible_objects(user_or_attribute, operation, options = {})
if policy_machine_storage_adapter.respond_to?(:accessible_objects)
policy_machine_storage_adapter.accessible_objects(user_or_attribute, operation, options)
else
result = objects.select { |object| is_privilege?(user_or_attribute, operation, object, options) }
if inclusion = options[:includes]
result.select! { |object| object.unique_identifier.include?(inclusion) }
end
result
end
end

##
# Returns an array of all user_attributes a PM::User is assigned to,
# directly or indirectly.
Expand Down
35 changes: 30 additions & 5 deletions lib/policy_machine_storage_adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -345,16 +345,41 @@ def scoped_privileges(user_or_attribute, object_or_attribute, options = {})
end
end

## Optimized version of PolicyMachine#scoped_privileges
# Returns all objects the user has the given operation on
# TODO: Support multiple policy classes here
def accessible_objects(user_or_attribute, operation, options = {})
operation = class_for_type('operation').find_by_unique_identifier!(operation.to_s) unless operation.is_a?(class_for_type('operation'))
permitting_oas = PolicyElement.where(id: operation.policy_element_associations.where(
user_attribute_id: user_or_attribute.descendants | [user_or_attribute],
).select(:object_attribute_id))
direct_scope = permitting_oas.where(type: class_for_type('object'))
indirect_scope = Assignment.ancestors_of(permitting_oas).where(type: class_for_type('object'))
if inclusion = options[:includes]
direct_scope = Adapter.apply_include_condition(scope: direct_scope, key: options[:key], value: inclusion, klass: class_for_type('object'))
indirect_scope = Adapter.apply_include_condition(scope: indirect_scope, key: options[:key], value: inclusion, klass: class_for_type('object'))
end
candidates = direct_scope | indirect_scope
if options[:ignore_prohibitions] || !(prohibition = class_for_type('operation').find_by_unique_identifier("~#{operation.unique_identifier}"))
candidates
else
candidates - accessible_objects(user_or_attribute, prohibition, options.merge(ignore_prohibitions: true))
end
end

private

def is_privilege_single_policy_class(user_or_attribute, operation, object_or_attribute)
def relevant_associations(user_or_attribute, operation, object_or_attribute)
if operation.is_a?(class_for_type('operation'))
associations_between(user_or_attribute, object_or_attribute).where(id: operation.policy_element_associations).any?
associations_between(user_or_attribute, object_or_attribute).where(id: operation.policy_element_associations)
else
associations_between(user_or_attribute, object_or_attribute).joins(:operations).where(policy_elements: {unique_identifier: operation}).any?
associations_between(user_or_attribute, object_or_attribute).joins(:operations).where(policy_elements: {unique_identifier: operation})
end
end

def is_privilege_single_policy_class(user_or_attribute, operation, object_or_attribute)
relevant_associations(user_or_attribute, operation, object_or_attribute).any?
end

def is_privilege_multiple_policy_classes(user_or_attribute, operation, object_or_attribute, policy_classes_containing_object)
#Outstanding active record sql adapter prevents chaining an additional where using the association.
Expand Down Expand Up @@ -392,8 +417,8 @@ def scoped_privileges_multiple_policy_classes(user_or_attribute, object_or_attri

def associations_between(user_or_attribute, object_or_attribute)
class_for_type('policy_element_association').where(
object_attribute_id: object_or_attribute.descendants | [object_or_attribute],
user_attribute_id: user_or_attribute.descendants | [user_or_attribute]
object_attribute_id: Assignment.descendants_of(object_or_attribute) | [object_or_attribute],
user_attribute_id: Assignment.descendants_of(user_or_attribute) | [user_or_attribute]
)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ def self.transitive_closure?(ancestor, descendant)
descendants_of(ancestor).include?(descendant)
end

def self.descendants_of(element)
def self.descendants_of(element_or_scope)
recursive_query = join_recursive do |query|
query.start_with(parent_id: element.id).connect_by(child_id: :parent_id).nocycle
query.start_with(parent_id: element_or_scope).connect_by(child_id: :parent_id).nocycle
end
PolicyElement.where(id: recursive_query.select('assignments.child_id'))
end

def self.ancestors_of(element)
def self.ancestors_of(element_or_scope)
recursive_query = join_recursive do |query|
query.start_with(child_id: element.id).connect_by(parent_id: :child_id).nocycle
query.start_with(child_id: element_or_scope).connect_by(parent_id: :child_id).nocycle
end
PolicyElement.where(id: recursive_query.select('assignments.parent_id'))
end
Expand Down
6 changes: 6 additions & 0 deletions lib/policy_machine_storage_adapters/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,11 @@ def scoped_privileges(user_or_attribute, object_or_attribute)

end

# Optimized version of PolicyMachine#accessible_objects
# Return all objects the user has the given operation on
# Optional: only add this method if you can do it better than policy_machine.rb
def accessible_objects(user_or_attribute, operation, options = {})
end

end
end
50 changes: 49 additions & 1 deletion spec/support/shared_examples_policy_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@
@u.foo.should == 'baz'
policy_machine.users.last.foo.should == 'baz'
end

it 'updates persisted extra attributes with new keys' do
@u = policy_machine.create_user('u1', foo: 'bar')
@u.update(foo: 'baz', bla: 'bar')
Expand Down Expand Up @@ -772,4 +772,52 @@
end
end

describe 'accessible_objects' do

before do
@one_fish = policy_machine.create_object('one:fish')
@two_fish = policy_machine.create_object('two:fish')
@red_one = policy_machine.create_object('red:one')
@read = policy_machine.create_operation('read')
@write = policy_machine.create_operation('write')
@u1 = policy_machine.create_user('u1')
@ua = policy_machine.create_user_attribute('ua')
[@one_fish, @two_fish, @red_one].each do |object|
policy_machine.add_association(@ua, Set.new([@read]), object)
end
@oa = policy_machine.create_object_attribute('oa')
policy_machine.add_association(@ua, Set.new([@write]), @oa)
policy_machine.add_assignment(@u1, @ua)
policy_machine.add_assignment(@red_one, @oa)
end

it 'lists all objects with the given privilege for the given user' do
expect( policy_machine.accessible_objects(@u1, @read, key: :unique_identifier).map(&:unique_identifier) ).to include('one:fish','two:fish','red:one')
expect( policy_machine.accessible_objects(@u1, @write, key: :unique_identifier).map(&:unique_identifier) ).to eq( ['red:one'] )
end

it 'filters objects via substring matching' do
expect( policy_machine.accessible_objects(@u1, @read, includes: 'fish', key: :unique_identifier).map(&:unique_identifier) ).to match_array(['one:fish','two:fish'])
expect( policy_machine.accessible_objects(@u1, @read, includes: 'one', key: :unique_identifier).map(&:unique_identifier) ).to match_array(['one:fish','red:one'])
end

context 'with prohibitions' do
before do
@oa2 = policy_machine.create_object_attribute('oa2')
policy_machine.add_assignment(@one_fish, @oa2)
policy_machine.add_association(@ua, Set.new([@read.prohibition]), @oa2)
end

it 'filters out prohibited objects by default' do
expect( policy_machine.accessible_objects(@u1, @read).map(&:unique_identifier) ).to match_array(['two:fish','red:one'])
end

it 'can ignore prohibitions' do
expect( policy_machine.accessible_objects(@u1, @read, ignore_prohibitions: true).map(&:unique_identifier) ).to match_array(['one:fish', 'two:fish','red:one'])
end

end

end

end

0 comments on commit 601bddd

Please sign in to comment.