policy_machine 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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