policy_machine 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/CONTRIBUTING.md +35 -0
  2. data/Gemfile +2 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +98 -0
  5. data/lib/generators/policy_machine/policy_machine_generator.rb +13 -0
  6. data/lib/generators/policy_machine/templates/migration.rb +40 -0
  7. data/lib/policy_machine.rb +236 -0
  8. data/lib/policy_machine/association.rb +73 -0
  9. data/lib/policy_machine/policy_element.rb +269 -0
  10. data/lib/policy_machine/version.rb +3 -0
  11. data/lib/policy_machine_storage_adapters/active_record.rb +306 -0
  12. data/lib/policy_machine_storage_adapters/in_memory.rb +266 -0
  13. data/lib/policy_machine_storage_adapters/neography.rb +236 -0
  14. data/lib/policy_machine_storage_adapters/template.rb +169 -0
  15. data/lib/tasks/policy_machine_tasks.rake +4 -0
  16. data/policy_machine.gemspec +23 -0
  17. data/spec/policy_machine/association_spec.rb +61 -0
  18. data/spec/policy_machine/policy_element_spec.rb +20 -0
  19. data/spec/policy_machine_spec.rb +7 -0
  20. data/spec/policy_machine_storage_adapters/active_record_spec.rb +54 -0
  21. data/spec/policy_machine_storage_adapters/in_memory_spec.rb +13 -0
  22. data/spec/policy_machine_storage_adapters/neography_spec.rb +42 -0
  23. data/spec/policy_machine_storage_adapters/template_spec.rb +6 -0
  24. data/spec/spec_helper.rb +24 -0
  25. data/spec/support/neography_helpers.rb +39 -0
  26. data/spec/support/policy_machine_helpers.rb +22 -0
  27. data/spec/support/shared_examples_policy_machine_spec.rb +697 -0
  28. data/spec/support/shared_examples_policy_machine_storage_adapter_spec.rb +278 -0
  29. data/spec/support/shared_examples_storage_adapter_public_methods.rb +20 -0
  30. data/spec/support/storage_adapter_helpers.rb +7 -0
  31. data/test/dummy/Rakefile +7 -0
  32. data/test/dummy/app/controllers/application_controller.rb +3 -0
  33. data/test/dummy/app/helpers/application_helper.rb +2 -0
  34. data/test/dummy/app/models/.gitkeep +0 -0
  35. data/test/dummy/config.ru +4 -0
  36. data/test/dummy/config/application.rb +65 -0
  37. data/test/dummy/config/boot.rb +10 -0
  38. data/test/dummy/config/database.yml +42 -0
  39. data/test/dummy/config/environment.rb +5 -0
  40. data/test/dummy/config/environments/development.rb +37 -0
  41. data/test/dummy/config/environments/test.rb +37 -0
  42. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  43. data/test/dummy/config/initializers/inflections.rb +15 -0
  44. data/test/dummy/config/initializers/mime_types.rb +5 -0
  45. data/test/dummy/config/initializers/secret_token.rb +7 -0
  46. data/test/dummy/config/initializers/session_store.rb +8 -0
  47. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/test/dummy/config/routes.rb +58 -0
  49. data/test/dummy/db/migrate/20131015214828_generate_policy_machine.rb +40 -0
  50. data/test/dummy/db/migrate/20131021221759_add_color_to_policy_element.rb +5 -0
  51. data/test/dummy/db/schema.rb +57 -0
  52. data/test/dummy/lib/assets/.gitkeep +0 -0
  53. data/test/dummy/script/rails +6 -0
  54. data/test/policy_machine_test.rb +7 -0
  55. data/test/test_helper.rb +15 -0
  56. metadata +270 -0
@@ -0,0 +1,73 @@
1
+ module PM
2
+ class Association
3
+ attr_accessor :user_attribute
4
+ attr_accessor :operation_set
5
+ attr_accessor :object_attribute
6
+
7
+ def initialize(stored_user_attribute, stored_operation_set, stored_object_attribute, pm_storage_adapter)
8
+ @user_attribute = PM::PolicyElement.convert_stored_pe_to_pe(
9
+ stored_user_attribute,
10
+ pm_storage_adapter,
11
+ PM::UserAttribute
12
+ )
13
+
14
+ @operation_set = Set.new
15
+ stored_operation_set.each do |stored_op|
16
+ op = PM::PolicyElement.convert_stored_pe_to_pe(
17
+ stored_op,
18
+ pm_storage_adapter,
19
+ PM::Operation
20
+ )
21
+ @operation_set << op
22
+ end
23
+
24
+ @object_attribute = PM::PolicyElement.convert_stored_pe_to_pe(
25
+ stored_object_attribute,
26
+ pm_storage_adapter,
27
+ PM::ObjectAttribute
28
+ )
29
+ end
30
+
31
+ # Returns true if the operation set of this association includes the given operation.
32
+ #
33
+ def includes_operation?(operation)
34
+ # Note: operation_set.member? isn't calling PM::PolicyElement ==
35
+ operation_set.any?{ |op| op == operation }
36
+ end
37
+
38
+ # Create an association given persisted policy elements
39
+ #
40
+ def self.create(user_attribute_pe, operation_set, object_attribute_pe, policy_machine_uuid, pm_storage_adapter)
41
+ # argument errors for user_attribute_pe
42
+ raise(ArgumentError, "user_attribute_pe must be a UserAttribute.") unless user_attribute_pe.is_a?(PM::UserAttribute)
43
+ unless user_attribute_pe.policy_machine_uuid == policy_machine_uuid
44
+ raise(ArgumentError, "user_attribute_pe must be in policy machine with uuid #{policy_machine_uuid}")
45
+ end
46
+
47
+ # argument errors for operation_set
48
+ raise(ArgumentError, "operation_set must be a Set of Operations") unless operation_set.is_a?(Set)
49
+ raise(ArgumentError, "operation_set must not be empty") if operation_set.empty?
50
+ operation_set.each do |op|
51
+ unless op.is_a?(PM::Operation)
52
+ raise(ArgumentError, "expected #{op} to be PM::Operation; got #{op.class}")
53
+ end
54
+ unless op.policy_machine_uuid == policy_machine_uuid
55
+ raise(ArgumentError, "expected #{op.unique_identifier} to be in Policy Machine with uuid #{policy_machine_uuid}; got #{op.policy_machine_uuid}")
56
+ end
57
+ end
58
+
59
+ # argument errors for object_attribute_pe
60
+ raise(ArgumentError, "object_attribute_pe must be an ObjectAttribute.") unless object_attribute_pe.is_a?(PM::ObjectAttribute)
61
+ unless object_attribute_pe.policy_machine_uuid == policy_machine_uuid
62
+ raise(ArgumentError, "object_attribute_pe must be in policy machine with uuid #{policy_machine_uuid}")
63
+ end
64
+
65
+ new_assoc = pm_storage_adapter.add_association(
66
+ user_attribute_pe.stored_pe,
67
+ operation_set.map(&:stored_pe),
68
+ object_attribute_pe.stored_pe,
69
+ policy_machine_uuid
70
+ )
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,269 @@
1
+ module PM
2
+
3
+ # A generic policy element in a policy machine.
4
+ # A policy element can be a user, user attribute, object, object attribute
5
+ # or operation set.
6
+ # This is an abstract base class and should not itself be instantiated.
7
+ class PolicyElement
8
+ attr_accessor :unique_identifier
9
+ attr_accessor :policy_machine_uuid
10
+ attr_accessor :stored_pe
11
+ attr_accessor :extra_attributes
12
+
13
+ ##
14
+ # Create a new policy element with the given name and type.
15
+ def initialize(unique_identifier, policy_machine_uuid, pm_storage_adapter, stored_pe = nil, extra_attributes = {})
16
+ @unique_identifier = unique_identifier.to_s
17
+ @policy_machine_uuid = policy_machine_uuid.to_s
18
+ @pm_storage_adapter = pm_storage_adapter
19
+ @stored_pe = stored_pe
20
+ @extra_attributes = extra_attributes
21
+ methodize_extra_attributes!
22
+ end
23
+
24
+ ##
25
+ # Determine if self is connected to other node
26
+ def connected?(other_pe)
27
+ @pm_storage_adapter.connected?(self.stored_pe, other_pe.stored_pe)
28
+ end
29
+
30
+ ##
31
+ # Assign self to destination policy element
32
+ # This method is sensitive to the type of self and dst_policy_element
33
+ #
34
+ def assign_to(dst_policy_element)
35
+ unless allowed_assignee_classes.any?{|aac| dst_policy_element.is_a?(aac)}
36
+ raise(ArgumentError, "expected dst_policy_element to be one of #{allowed_assignee_classes.to_s}; got #{dst_policy_element.class} instead.")
37
+ end
38
+ @pm_storage_adapter.assign(self.stored_pe, dst_policy_element.stored_pe)
39
+ end
40
+
41
+ ##
42
+ # Remove assignment from self to destination policy element
43
+ # Returns boolean indicating whether assignment was successfully removed.
44
+ #
45
+ def unassign(dst_policy_element)
46
+ @pm_storage_adapter.unassign(self.stored_pe, dst_policy_element.stored_pe)
47
+ end
48
+
49
+ ##
50
+ # Remove self, and any assignments to or from self. Does not remove any other elements.
51
+ # Returns true if persisted object was successfully removed.
52
+ #
53
+ def delete
54
+ if self.stored_pe && self.stored_pe.persisted
55
+ @pm_storage_adapter.delete(stored_pe)
56
+ self.stored_pe = nil
57
+ true
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Updates extra attributes with the passed-in values. Will not remove other
63
+ # attributes not in the hash. Returns true if no errors occurred.
64
+ #
65
+ def update(attr_hash)
66
+ @extra_attributes.merge!(attr_hash)
67
+ methodize_extra_attributes!
68
+ if self.stored_pe && self.stored_pe.persisted
69
+ @pm_storage_adapter.update(self.stored_pe, attr_hash)
70
+ true
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Convert a stored_pe to an instantiated pe
76
+ def self.convert_stored_pe_to_pe(stored_pe, pm_storage_adapter, pe_class)
77
+ pe_class.new(
78
+ stored_pe.unique_identifier,
79
+ stored_pe.policy_machine_uuid,
80
+ pm_storage_adapter,
81
+ stored_pe
82
+ )
83
+ end
84
+
85
+ ##
86
+ # Returns true if self is identical to other and false otherwise.
87
+ #
88
+ def ==(other_pe)
89
+ self.class == other_pe.class &&
90
+ self.unique_identifier == other_pe.unique_identifier &&
91
+ self.policy_machine_uuid == other_pe.policy_machine_uuid
92
+ end
93
+
94
+ ##
95
+ # Delegate extra attribute reads to stored_pe
96
+ #
97
+ def method_missing(meth, *args)
98
+ if args.none? && stored_pe.respond_to?(meth)
99
+ stored_pe.send(meth)
100
+ else
101
+ super
102
+ end
103
+ end
104
+
105
+ def respond_to_missing?(meth, include_private = false)
106
+ stored_pe.respond_to?(meth, include_private) || super
107
+ end
108
+
109
+ protected
110
+ def allowed_assignee_classes
111
+ raise "Must override this method in a subclass"
112
+ end
113
+
114
+ ##
115
+ # Allow magic attribute methods like in ActiveRecord
116
+ #
117
+ def methodize_extra_attributes!
118
+ @extra_attributes.keys.each do |attr|
119
+ define_singleton_method attr, lambda {@extra_attributes[attr]} unless respond_to?(attr)
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ # TODO: there is repeated code in the following subclasses which I will DRY in the
126
+ # next PR.
127
+ # A user in a policy machine.
128
+ class User < PolicyElement
129
+ def self.create(unique_identifier, policy_machine_uuid, pm_storage_adapter, extra_attributes = {})
130
+ new_pe = new(unique_identifier, policy_machine_uuid, pm_storage_adapter, nil, extra_attributes)
131
+ new_pe.stored_pe = pm_storage_adapter.add_user(unique_identifier, policy_machine_uuid, extra_attributes)
132
+ new_pe
133
+ end
134
+
135
+ def user_attributes(pm_storage_adapter)
136
+ pm_storage_adapter.user_attributes_for_user(stored_pe).map do |stored_ua|
137
+ self.class.convert_stored_pe_to_pe(stored_ua, pm_storage_adapter, PM::UserAttribute)
138
+ end
139
+ end
140
+
141
+ # Return all policy elements of a particular type (e.g. all users)
142
+ def self.all(pm_storage_adapter, options = {})
143
+ pm_storage_adapter.find_all_of_type_user(options).map do |stored_pe|
144
+ convert_stored_pe_to_pe(stored_pe, pm_storage_adapter, PM::User)
145
+ end
146
+ end
147
+
148
+ protected
149
+ def allowed_assignee_classes
150
+ [UserAttribute]
151
+ end
152
+ end
153
+
154
+ # A user attribute in a policy machine.
155
+ class UserAttribute < PolicyElement
156
+ def self.create(unique_identifier, policy_machine_uuid, pm_storage_adapter, extra_attributes = {})
157
+ new_pe = new(unique_identifier, policy_machine_uuid, pm_storage_adapter, nil, extra_attributes)
158
+ new_pe.stored_pe = pm_storage_adapter.add_user_attribute(unique_identifier, policy_machine_uuid, extra_attributes)
159
+ new_pe
160
+ end
161
+
162
+ # Return all policy elements of a particular type (e.g. all users)
163
+ def self.all(pm_storage_adapter, options = {})
164
+ pm_storage_adapter.find_all_of_type_user_attribute(options).map do |stored_pe|
165
+ convert_stored_pe_to_pe(stored_pe, pm_storage_adapter, PM::UserAttribute)
166
+ end
167
+ end
168
+
169
+ protected
170
+ def allowed_assignee_classes
171
+ [UserAttribute, PolicyClass]
172
+ end
173
+ end
174
+
175
+ # An object attribute in a policy machine.
176
+ class ObjectAttribute < PolicyElement
177
+ def self.create(unique_identifier, policy_machine_uuid, pm_storage_adapter, extra_attributes = {})
178
+ new_pe = new(unique_identifier, policy_machine_uuid, pm_storage_adapter, nil, extra_attributes)
179
+ new_pe.stored_pe = pm_storage_adapter.add_object_attribute(unique_identifier, policy_machine_uuid, extra_attributes)
180
+ new_pe
181
+ end
182
+
183
+ # Returns an array of policy classes in which this ObjectAttribute is included.
184
+ # Returns empty array if this ObjectAttribute is associated with no policy classes.
185
+ def policy_classes
186
+ pcs_for_object = @pm_storage_adapter.policy_classes_for_object_attribute(stored_pe)
187
+ pcs_for_object.map do |stored_pc|
188
+ self.class.convert_stored_pe_to_pe(stored_pc, @pm_storage_adapter, PM::PolicyClass)
189
+ end
190
+ end
191
+
192
+ def self.all(pm_storage_adapter, options = {})
193
+ pm_storage_adapter.find_all_of_type_object_attribute(options).map do |stored_pe|
194
+ convert_stored_pe_to_pe(stored_pe, pm_storage_adapter, PM::ObjectAttribute)
195
+ end
196
+ end
197
+
198
+ protected
199
+ def allowed_assignee_classes
200
+ [ObjectAttribute, PolicyClass]
201
+ end
202
+ end
203
+
204
+ # An object in a policy machine.
205
+ class Object < ObjectAttribute
206
+ def self.create(unique_identifier, policy_machine_uuid, pm_storage_adapter, extra_attributes = {})
207
+ new_pe = new(unique_identifier, policy_machine_uuid, pm_storage_adapter, nil, extra_attributes)
208
+ new_pe.stored_pe = pm_storage_adapter.add_object(unique_identifier, policy_machine_uuid, extra_attributes)
209
+ new_pe
210
+ end
211
+
212
+ # Return all policy elements of a particular type (e.g. all users)
213
+ def self.all(pm_storage_adapter, options = {})
214
+ pm_storage_adapter.find_all_of_type_object(options).map do |stored_pe|
215
+ convert_stored_pe_to_pe(stored_pe, pm_storage_adapter, PM::Object)
216
+ end
217
+ end
218
+
219
+ protected
220
+ def allowed_assignee_classes
221
+ [Object, ObjectAttribute]
222
+ end
223
+ end
224
+
225
+ # An operation set in a policy machine.
226
+ class Operation < PolicyElement
227
+ def self.create(unique_identifier, policy_machine_uuid, pm_storage_adapter, extra_attributes = {})
228
+ new_pe = new(unique_identifier, policy_machine_uuid, pm_storage_adapter, nil, extra_attributes)
229
+ new_pe.stored_pe = pm_storage_adapter.add_operation(unique_identifier, policy_machine_uuid, extra_attributes)
230
+ new_pe
231
+ end
232
+
233
+ # Return all policy elements of a particular type (e.g. all users)
234
+ def self.all(pm_storage_adapter, options = {})
235
+ pm_storage_adapter.find_all_of_type_operation(options).map do |stored_pe|
236
+ convert_stored_pe_to_pe(stored_pe, pm_storage_adapter, PM::Operation)
237
+ end
238
+ end
239
+
240
+ # Return all associations in which this Operation is included
241
+ # Associations are arrays of PM::Attributes.
242
+ #
243
+ def associations
244
+ @pm_storage_adapter.associations_with(self.stored_pe).map do |assoc|
245
+ PM::Association.new(assoc[0], assoc[1], assoc[2], @pm_storage_adapter)
246
+ end
247
+ end
248
+
249
+ protected
250
+ def allowed_assignee_classes
251
+ []
252
+ end
253
+ end
254
+
255
+ # A policy class in a policy machine.
256
+ class PolicyClass < PolicyElement
257
+ def self.create(unique_identifier, policy_machine_uuid, pm_storage_adapter, extra_attributes = {})
258
+ new_pe = new(unique_identifier, policy_machine_uuid, pm_storage_adapter, nil, extra_attributes)
259
+ new_pe.stored_pe = pm_storage_adapter.add_policy_class(unique_identifier, policy_machine_uuid, extra_attributes)
260
+ new_pe
261
+ end
262
+
263
+ protected
264
+ def allowed_assignee_classes
265
+ []
266
+ end
267
+ end
268
+
269
+ end
@@ -0,0 +1,3 @@
1
+ module PolicyMachine
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,306 @@
1
+ require 'policy_machine'
2
+ require 'set'
3
+
4
+ # This class stores policy elements in a SQL database using whatever
5
+ # database configuration and adapters are provided by active_record.
6
+ # Currently only MySQL is supported via this adapter.
7
+
8
+ begin
9
+ require 'active_record'
10
+ rescue LoadError
11
+ active_record_unavailable = true
12
+ end
13
+
14
+ module PolicyMachineStorageAdapter
15
+
16
+ class ActiveRecord
17
+
18
+ class PolicyElement < ::ActiveRecord::Base
19
+ alias :persisted :persisted?
20
+ # needs unique_identifier, policy_machine_uuid, type, extra_attributes columns
21
+ has_many :assignments, foreign_key: :parent_id, dependent: :destroy
22
+ has_many :children, through: :assignments, dependent: :destroy #this doesn't actually destroy the children, just the assignment
23
+ has_many :transitive_closure, foreign_key: :ancestor_id
24
+ has_many :descendants, through: :transitive_closure
25
+ attr_accessible :unique_identifier, :policy_machine_uuid, :extra_attributes
26
+ attr_accessor :extra_attributes_hash
27
+ before_save :serialize_extra_attributes_hash
28
+
29
+ def method_missing(meth, *args, &block)
30
+ methodize_extra_attributes_hash
31
+ if respond_to?(meth)
32
+ send(meth, *args)
33
+ elsif meth.to_s[-1] == '='
34
+ @extra_attributes_hash[meth.to_s] = args.first
35
+ methodize_extra_attributes_hash
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ def respond_to_missing?(meth, *args)
42
+ methodize_extra_attributes_hash unless @extra_attributes_hash
43
+ @extra_attributes_hash[meth.to_s] || super
44
+ end
45
+
46
+ def methodize_extra_attributes_hash
47
+ @extra_attributes_hash = JSON.parse(self.extra_attributes, quirks_mode: true) if self.extra_attributes
48
+ @extra_attributes_hash ||= {}
49
+ @extra_attributes_hash.extract!(*self.class.column_names).each do |key, value|
50
+ send("#{key}=", value) unless value.nil?
51
+ end
52
+ @extra_attributes_hash.each do |key, value|
53
+ define_singleton_method key, lambda {@extra_attributes_hash[key.to_s]}
54
+ define_singleton_method "#{key}=", lambda { | value | @extra_attributes_hash[key.to_s] = value }
55
+ end
56
+ end
57
+
58
+ def serialize_extra_attributes_hash
59
+ methodize_extra_attributes_hash unless @extra_attributes_hash
60
+ self.extra_attributes = extra_attributes_hash.to_json
61
+ end
62
+
63
+ end
64
+
65
+ class User < PolicyElement
66
+ end
67
+
68
+ class UserAttribute < PolicyElement
69
+ has_many :policy_element_associations, dependent: :destroy
70
+ end
71
+
72
+ class ObjectAttribute < PolicyElement
73
+ has_many :policy_element_associations, dependent: :destroy
74
+ end
75
+
76
+ class Object < ObjectAttribute
77
+ end
78
+
79
+ class Operation < PolicyElement
80
+ has_and_belongs_to_many :policy_element_associations, class_name: :"PolicyMachineStorageAdapter::ActiveRecord::PolicyElementAssociation"
81
+ end
82
+
83
+ class PolicyClass < PolicyElement
84
+ end
85
+
86
+ class PolicyElementAssociation < ::ActiveRecord::Base
87
+ # requires a join table (should be indexed!)
88
+ has_and_belongs_to_many :operations, class_name: :"PolicyMachineStorageAdapter::ActiveRecord::Operation"
89
+ belongs_to :user_attribute
90
+ belongs_to :object_attribute
91
+ end
92
+
93
+ class TransitiveClosure < ::ActiveRecord::Base
94
+ self.table_name = 'transitive_closure'
95
+ # needs ancestor_id, descendant_id columns
96
+ belongs_to :ancestor, class_name: :PolicyElement
97
+ belongs_to :descendant, class_name: :PolicyElement
98
+ end
99
+
100
+ class Assignment < ::ActiveRecord::Base
101
+ # needs parent_id, child_id columns
102
+ after_create :add_to_transitive_closure
103
+ after_destroy :remove_from_transitive_closure
104
+ belongs_to :parent, class_name: :PolicyElement
105
+ belongs_to :child, class_name: :PolicyElement
106
+
107
+ def self.transitive_closure?(ancestor, descendant)
108
+ TransitiveClosure.exists?(ancestor_id: ancestor.id, descendant_id: descendant.id)
109
+ end
110
+
111
+ def add_to_transitive_closure
112
+ connection.execute("Insert ignore into transitive_closure values (#{parent_id}, #{child_id})")
113
+ connection.execute("Insert ignore into transitive_closure
114
+ select parents_ancestors.ancestor_id, childs_descendants.descendant_id from
115
+ transitive_closure parents_ancestors,
116
+ transitive_closure childs_descendants
117
+ where
118
+ (parents_ancestors.descendant_id = #{parent_id} or parents_ancestors.ancestor_id = #{parent_id})
119
+ and (childs_descendants.ancestor_id = #{child_id} or childs_descendants.descendant_id = #{child_id})")
120
+ end
121
+
122
+ def remove_from_transitive_closure
123
+ parents_ancestors = connection.execute("Select ancestor_id from transitive_closure where descendant_id=#{parent_id}")
124
+ childs_descendants = connection.execute("Select descendant_id from transitive_closure where ancestor_id=#{child_id}")
125
+ parents_ancestors = parents_ancestors.to_a.<<(parent_id).join(',')
126
+ childs_descendants = childs_descendants.to_a.<<(child_id).join(',')
127
+
128
+ connection.execute("Delete from transitive_closure where
129
+ ancestor_id in (#{parents_ancestors}) and
130
+ descendant_id in (#{childs_descendants}) and
131
+ not exists (Select * from assignments where parent_id=ancestor_id and child_id=descendant_id)
132
+ ")
133
+
134
+ connection.execute("Insert ignore into transitive_closure
135
+ select ancestors_surviving_relationships.ancestor_id, descendants_surviving_relationships.descendant_id
136
+ from
137
+ transitive_closure ancestors_surviving_relationships,
138
+ transitive_closure descendants_surviving_relationships
139
+ where
140
+ (ancestors_surviving_relationships.ancestor_id in (#{parents_ancestors}))
141
+ and (descendants_surviving_relationships.descendant_id in (#{childs_descendants}))
142
+ and (ancestors_surviving_relationships.descendant_id = descendants_surviving_relationships.ancestor_id)
143
+ ")
144
+ end
145
+
146
+ end
147
+
148
+ POLICY_ELEMENT_TYPES = %w(user user_attribute object object_attribute operation policy_class)
149
+
150
+ POLICY_ELEMENT_TYPES.each do |pe_type|
151
+ ##
152
+ # Store a policy element of type pe_type.
153
+ # The unique_identifier identifies the element within the policy machine.
154
+ # The policy_machine_uuid is the uuid of the containing policy machine.
155
+ #
156
+ define_method("add_#{pe_type}") do |unique_identifier, policy_machine_uuid, extra_attributes = {}|
157
+ active_record_attributes = extra_attributes.stringify_keys
158
+ extra_attributes = active_record_attributes.slice!(*PolicyElement.column_names)
159
+ element_attrs = {
160
+ :unique_identifier => unique_identifier,
161
+ :policy_machine_uuid => policy_machine_uuid,
162
+ :extra_attributes => extra_attributes.to_json
163
+ }.merge(active_record_attributes)
164
+ class_for_type(pe_type).create(element_attrs, without_protection: true)
165
+ end
166
+
167
+ define_method("find_all_of_type_#{pe_type}") do |options = {}|
168
+ conditions = options.stringify_keys
169
+ extra_attribute_conditions = conditions.slice!(*PolicyElement.column_names)
170
+ all = class_for_type(pe_type).where(conditions)
171
+ extra_attribute_conditions.each do |key, value|
172
+ warn "WARNING: #{self.class} is filtering #{pe_type} on #{key} in memory, which won't scale well. " <<
173
+ "To move this query to the database, add a '#{key}' column to the policy_elements table " <<
174
+ "and re-save existing records"
175
+ all.select!{ |pe| pe.methodize_extra_attributes_hash and pe.extra_attributes_hash[key] == value }
176
+ end
177
+ all
178
+ end
179
+ end
180
+
181
+ def class_for_type(pe_type)
182
+ @pe_type_class_hash ||= Hash.new { |h,k| h[k] = "PolicyMachineStorageAdapter::ActiveRecord::#{k.camelize}".constantize }
183
+ @pe_type_class_hash[pe_type]
184
+ end
185
+
186
+ ##
187
+ # Assign src to dst in policy machine.
188
+ # The two policy elements must be persisted policy elements
189
+ # Returns true if the assignment occurred, false otherwise.
190
+ #
191
+ def assign(src, dst)
192
+ assert_persisted_policy_element(src, dst)
193
+ src.children << dst
194
+ end
195
+
196
+ ##
197
+ # Determine if there is a path from src to dst in the policy machine.
198
+ # The two policy elements must be persisted policy elements; otherwise the method should raise
199
+ # an ArgumentError.
200
+ # Returns true if there is a such a path and false otherwise.
201
+ # Should return true if src == dst
202
+ #
203
+ def connected?(src, dst)
204
+ assert_persisted_policy_element(src, dst)
205
+ src == dst || Assignment.transitive_closure?(src, dst)
206
+ end
207
+
208
+ ##
209
+ # Disconnect two policy elements in the machine
210
+ # The two policy elements must be persisted policy elements; otherwise the method should raise
211
+ # an ArgumentError.
212
+ # Returns true if unassignment occurred and false otherwise.
213
+ # Generally, false will be returned if the assignment didn't exist in the PM in the
214
+ # first place.
215
+ #
216
+ def unassign(src, dst)
217
+ assert_persisted_policy_element(src, dst)
218
+ if assignment = src.assignments.where(child_id: dst.id).first
219
+ assignment.destroy
220
+ end
221
+ end
222
+
223
+ ##
224
+ # Remove a persisted policy element. This should remove its assignments and
225
+ # associations but must not cascade to any connected policy elements.
226
+ # Returns true if the delete succeeded.
227
+ #
228
+ def delete(element)
229
+ element.destroy
230
+ end
231
+
232
+ ##
233
+ # Update the extra_attributes of a persisted policy element.
234
+ # This should only affect attributes corresponding to the keys passed in.
235
+ # Returns true if the update succeeded or was redundant.
236
+ #
237
+ def update(element, changes_hash)
238
+ changes_hash.each { |k,v| element.send("#{k}=",v) }
239
+ element.save
240
+ end
241
+
242
+ ##
243
+ # Determine if the given node is in the policy machine or not.
244
+ # Returns true or false accordingly.
245
+ # TODO: This seems wrong.
246
+ #
247
+ def element_in_machine?(pe)
248
+ pe.persisted?
249
+ end
250
+
251
+ ##
252
+ # Add the given association to the policy map. If an association between user_attribute
253
+ # and object_attribute already exists, then replace it with that given in the arguments.
254
+ # Returns true if the association was added and false otherwise.
255
+ #
256
+ def add_association(user_attribute, operation_set, object_attribute, policy_machine_uuid)
257
+ PolicyElementAssociation.where(
258
+ user_attribute_id: user_attribute.id,
259
+ object_attribute_id: object_attribute.id
260
+ ).first_or_create.operations = operation_set.to_a
261
+ end
262
+
263
+ ##
264
+ # Return an array of all associations in which the given operation is included.
265
+ # Each element of the array should itself be an array in which the first element
266
+ # is the user_attribute member of the association, the second element is a
267
+ # Ruby Set, each element of which is an operation, the third element is the
268
+ # object_attribute member of the association.
269
+ # If no associations are found then the empty array should be returned.
270
+ #
271
+ def associations_with(operation)
272
+ assocs = operation.policy_element_associations.all(include: [:user_attribute, :operations, :object_attribute])
273
+ assocs.map { |assoc| [assoc.user_attribute, Set.new(assoc.operations), assoc.object_attribute] }
274
+ end
275
+
276
+ ##
277
+ # Return array of all policy classes which contain the given object_attribute (or object).
278
+ # Return empty array if no such policy classes found.
279
+ def policy_classes_for_object_attribute(object_attribute)
280
+ object_attribute.descendants.where(type: class_for_type('policy_class'))
281
+ end
282
+
283
+ ##
284
+ # Return array of all user attributes which contain the given user.
285
+ # Return empty array if no such user attributes are found.
286
+ def user_attributes_for_user(user)
287
+ user.descendants.where(type: class_for_type('user_attribute'))
288
+ end
289
+
290
+ ##
291
+ # Execute the passed-in block transactionally: any error raised out of the block causes
292
+ # all the block's changes to be rolled back.
293
+ def transaction(&block)
294
+ PolicyElement.transaction(&block)
295
+ end
296
+
297
+ private
298
+
299
+ def assert_persisted_policy_element(*arguments)
300
+ arguments.each do |argument|
301
+ raise ArgumentError, "expected policy elements, got #{argument}" unless argument.is_a?(PolicyElement)
302
+ end
303
+ end
304
+
305
+ end
306
+ end unless active_record_unavailable