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,266 @@
1
+ require 'policy_machine'
2
+
3
+ # This class stores policy elements in memory and
4
+ # exposes required operations for managing/querying these elements.
5
+
6
+ module PolicyMachineStorageAdapter
7
+ class InMemory
8
+
9
+ POLICY_ELEMENT_TYPES = %w(user user_attribute object object_attribute operation policy_class)
10
+
11
+ POLICY_ELEMENT_TYPES.each do |pe_type|
12
+ ##
13
+ # Store a policy element of type pe_type.
14
+ # The unique_identifier identifies the element within the policy machine.
15
+ # The policy_machine_uuid is the uuid of the containing policy machine.
16
+ #
17
+ # TODO: add optional check to determine if unique_identifier is truly unique within
18
+ # given policy_machine.
19
+ #
20
+ define_method("add_#{pe_type}") do |unique_identifier, policy_machine_uuid, extra_attributes = {}|
21
+ persisted_pe = PersistedPolicyElement.new(unique_identifier, policy_machine_uuid, pe_type, extra_attributes)
22
+ persisted_pe.persisted = true
23
+ policy_elements << persisted_pe
24
+ persisted_pe
25
+ end
26
+
27
+ define_method("find_all_of_type_#{pe_type}") do |options = {}|
28
+ conditions = options.merge(pe_type: pe_type)
29
+ policy_elements.select do |pe|
30
+ conditions.all? do |k,v|
31
+ if v.nil?
32
+ !pe.respond_to?(k) || pe.send(k) == nil
33
+ else
34
+ pe.respond_to?(k) && pe.send(k) == v
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ ##
42
+ # Assign src to dst in policy machine
43
+ #
44
+ def assign(src, dst)
45
+ assert_persisted_policy_element(src)
46
+ assert_persisted_policy_element(dst)
47
+
48
+ assignments << [src, dst]
49
+ true
50
+ end
51
+
52
+ ##
53
+ # Determine if there is a path from src to dst in the policy machine
54
+ #
55
+ def connected?(src, dst)
56
+ assert_persisted_policy_element(src)
57
+ assert_persisted_policy_element(dst)
58
+
59
+ return true if src == dst
60
+
61
+ distances = dijkstra(src, dst)
62
+ distances.nil? ? false : true
63
+ end
64
+
65
+ ##
66
+ # Disconnect two policy elements in the machine
67
+ #
68
+ def unassign(src, dst)
69
+ assert_persisted_policy_element(src)
70
+ assert_persisted_policy_element(dst)
71
+
72
+ assignment = assignments.find{|assgn| assgn[0] == src && assgn[1] == dst}
73
+ if assignment
74
+ assignments.delete(assignment)
75
+ true
76
+ else
77
+ false
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Remove a persisted policy element
83
+ #
84
+ def delete(element)
85
+ assignments.delete_if{ |assgn| assgn.include?(element) }
86
+ associations.delete_if { |_,assoc| assoc.include?(element) }
87
+ policy_elements.delete(element)
88
+ end
89
+
90
+ ##
91
+ # Update a persisted policy element
92
+ #
93
+ def update(element, changes_hash)
94
+ element.send(:extra_attributes).merge!(changes_hash)
95
+ end
96
+
97
+ ##
98
+ # Determine if the given node is in the policy machine or not.
99
+ def element_in_machine?(pe)
100
+ policy_elements.member?( pe )
101
+ end
102
+
103
+ ##
104
+ # Add the given association to the policy map. If an association between user_attribute
105
+ # and object_attribute already exists, then replace it with that given in the arguments.
106
+ def add_association(user_attribute, operation_set, object_attribute, policy_machine_uuid)
107
+ # TODO: scope by policy machine uuid
108
+ associations[user_attribute.unique_identifier + object_attribute.unique_identifier] =
109
+ [user_attribute, operation_set, object_attribute]
110
+
111
+ true
112
+ end
113
+
114
+ ##
115
+ # Return all associations in which the given operation is included
116
+ # Returns an array of arrays. Each sub-array is of the form
117
+ # [user_attribute, operation_set, object_attribute]
118
+ def associations_with(operation)
119
+ matching = associations.values.select do |assoc|
120
+ assoc[1].include?(operation)
121
+ end
122
+
123
+ matching.map{ |m| [m[0], m[1], m[2]] }
124
+ end
125
+
126
+ ##
127
+ # Return array of all policy classes which contain the given object_attribute (or object).
128
+ # Return empty array if no such policy classes found.
129
+ def policy_classes_for_object_attribute(object_attribute)
130
+ find_all_of_type_policy_class.select do |pc|
131
+ connected?(object_attribute, pc)
132
+ end
133
+ end
134
+
135
+ ##
136
+ # Return array of all user attributes which contain the given user.
137
+ # Return empty array if no such user attributes are found.
138
+ def user_attributes_for_user(user)
139
+ find_all_of_type_user_attribute.select do |user_attribute|
140
+ connected?(user, user_attribute)
141
+ end
142
+ end
143
+
144
+ ##
145
+ # Execute the passed-in block transactionally: any error raised out of the block causes
146
+ # all the block's changes to be rolled back.
147
+ def transaction
148
+ old_state = dup
149
+ instance_variables.each do |var|
150
+ value = instance_variable_get(var)
151
+
152
+ if (value.respond_to?(:dup))
153
+ old_state.instance_variable_set(var, value.dup)
154
+ end
155
+ end
156
+
157
+ begin
158
+ yield
159
+ rescue Exception
160
+ instance_variables.each do |var|
161
+ value = old_state.instance_variable_get(var)
162
+ instance_variable_set(var, value)
163
+ end
164
+ raise
165
+ end
166
+ end
167
+
168
+
169
+ private
170
+
171
+ # Raise argument error if argument is not suitable for consumption in
172
+ # public methods.
173
+ def assert_persisted_policy_element(arg)
174
+ raise(ArgumentError, "arg must be a PersistedPolicyElement; got #{arg.class.name}") unless arg.is_a?(PersistedPolicyElement)
175
+ raise(ArgumentError, "arg must be persisted") unless element_in_machine?(arg)
176
+ end
177
+
178
+ # The policy elements in the persisted policy machine.
179
+ def policy_elements
180
+ @policy_elements ||= []
181
+ end
182
+
183
+ # The policy element assignments in the persisted policy machine.
184
+ def assignments
185
+ @assignments ||= []
186
+ end
187
+
188
+ # All persisted associations
189
+ def associations
190
+ @associations ||= {}
191
+ end
192
+
193
+ def dijkstra(src, dst = nil)
194
+ nodes = policy_elements
195
+
196
+ distances = {}
197
+ previouses = {}
198
+ nodes.each do |vertex|
199
+ distances[vertex] = nil # Infinity
200
+ previouses[vertex] = nil
201
+ end
202
+ distances[src] = 0
203
+ vertices = nodes.clone
204
+ until vertices.empty?
205
+ nearest_vertex = vertices.inject do |a, b|
206
+ next b unless distances[a]
207
+ next a unless distances[b]
208
+ next a if distances[a] < distances[b]
209
+ b
210
+ end
211
+ break unless distances[nearest_vertex] # Infinity
212
+ if dst and nearest_vertex == dst
213
+ return distances[dst]
214
+ end
215
+ neighbors = neighbors(nearest_vertex)
216
+ neighbors.each do |vertex|
217
+ alt = distances[nearest_vertex] + 1
218
+ if distances[vertex].nil? or alt < distances[vertex]
219
+ distances[vertex] = alt
220
+ previouses[vertices] = nearest_vertex
221
+ # decrease-key v in Q # ???
222
+ end
223
+ end
224
+ vertices.delete nearest_vertex
225
+ end
226
+
227
+ return nil
228
+ end
229
+
230
+ # Find all nodes which are directly connected to
231
+ # +node+
232
+ def neighbors(pe)
233
+ neighbors = []
234
+ assignments.each do |assignment|
235
+ neighbors.push assignment[1] if assignment[0] == pe
236
+ end
237
+ return neighbors.uniq
238
+ end
239
+
240
+ # Class to represent policy elements
241
+ class PersistedPolicyElement
242
+ attr_accessor :persisted
243
+ attr_reader :unique_identifier, :policy_machine_uuid, :pe_type, :extra_attributes
244
+
245
+ # Ensure that attr keys are strings
246
+ def initialize(unique_identifier, policy_machine_uuid, pe_type, extra_attributes)
247
+ @unique_identifier = unique_identifier
248
+ @policy_machine_uuid = policy_machine_uuid
249
+ @pe_type = pe_type
250
+ @persisted = false
251
+ @extra_attributes = extra_attributes
252
+ extra_attributes.each do |key, value|
253
+ define_singleton_method key, lambda {@extra_attributes[key]}
254
+ end
255
+ end
256
+
257
+ def ==(other)
258
+ return false unless other.is_a?(PersistedPolicyElement)
259
+ self.unique_identifier == other.unique_identifier &&
260
+ self.policy_machine_uuid == other.policy_machine_uuid &&
261
+ self.pe_type == other.pe_type
262
+ end
263
+
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,236 @@
1
+ begin
2
+ require 'neography'
3
+ rescue LoadError
4
+ neography_unavailable = true
5
+ end
6
+
7
+ # This class stores policy elements in a neo4j graph db using the neography client and
8
+ # exposes required operations for managing/querying these elements.
9
+ # Note that this adapter shouldn't be used in production for high-performance needs as Neography
10
+ # is inherently slower than more direct NEO4J access.
11
+ module PolicyMachineStorageAdapter
12
+ class Neography
13
+
14
+ POLICY_ELEMENT_TYPES = %w(user user_attribute object object_attribute operation policy_class)
15
+
16
+ POLICY_ELEMENT_TYPES.each do |pe_type|
17
+ ##
18
+ # Store a policy element of type pe_type.
19
+ # The unique_identifier identifies the element within the policy machine.
20
+ # The policy_machine_uuid is the uuid of the containing policy machine.
21
+ #
22
+ # TODO: add optional check to determine if unique_identifier is truly unique within
23
+ # given policy_machine.
24
+ #
25
+ define_method("add_#{pe_type}") do |unique_identifier, policy_machine_uuid, extra_attributes = {}|
26
+ node_attrs = {
27
+ :unique_identifier => unique_identifier,
28
+ :policy_machine_uuid => policy_machine_uuid,
29
+ :pe_type => pe_type,
30
+ :persisted => true
31
+ }.merge(extra_attributes)
32
+ persisted_pe = ::Neography::Node.create(node_attrs)
33
+ persisted_pe.add_to_index('nodes', 'unique_identifier', unique_identifier)
34
+ persisted_pe.add_to_index('policy_element_types', 'pe_type', pe_type)
35
+ persisted_pe
36
+ end
37
+
38
+ define_method("find_all_of_type_#{pe_type}") do |options = {}|
39
+ found_elts = ::Neography::Node.find('policy_element_types', 'pe_type', pe_type)
40
+ found_elts = found_elts.nil? ? [] : [found_elts].flatten
41
+ found_elts.select do |elt|
42
+ options.all? do |k,v|
43
+ if v.nil?
44
+ !elt.respond_to?(k)
45
+ else
46
+ elt.respond_to?(k) && elt.send(k) == v
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Assign src to dst in policy machine
55
+ #
56
+ def assign(src, dst)
57
+ assert_persisted_policy_element(src)
58
+ assert_persisted_policy_element(dst)
59
+
60
+ e = ::Neography::Relationship.create(:outgoing, src, dst)
61
+
62
+ if e.nil?
63
+ false
64
+ else
65
+ unique_identifier = src.unique_identifier + dst.unique_identifier
66
+ e.add_to_index('edges', 'unique_identifier', unique_identifier)
67
+ true
68
+ end
69
+ end
70
+
71
+ ##
72
+ # Determine if there is a path from src to dst in the policy machine
73
+ #
74
+ def connected?(src, dst)
75
+ assert_persisted_policy_element(src)
76
+ assert_persisted_policy_element(dst)
77
+
78
+ return true if src == dst
79
+
80
+ neo_connection.execute_query("start n=node({id1}),m=node({id2}) return (n)-[*]->(m)",
81
+ {:id1 => src.neo_id.to_i, :id2 => dst.neo_id.to_i})['data'] != [[[]]]
82
+ end
83
+
84
+ ##
85
+ # Disconnect two policy elements in the machine
86
+ #
87
+ def unassign(src, dst)
88
+ assert_persisted_policy_element(src)
89
+ assert_persisted_policy_element(dst)
90
+
91
+ unique_identifier = src.unique_identifier + dst.unique_identifier
92
+ found_edges = ::Neography::Relationship.find('edges', 'unique_identifier', unique_identifier)
93
+
94
+ if found_edges
95
+ # Neography::Relationship doesn't respond to .to_a
96
+ found_edges = [found_edges] unless found_edges.is_a?(Array)
97
+ found_edges.each do |found_edge|
98
+ # Unfortunately, we have to reload the edge as find isn't deserializing it properly.
99
+ e = ::Neography::Relationship.load(found_edge.neo_id.to_i)
100
+ e.del unless e.nil?
101
+ end
102
+ true
103
+ else
104
+ false
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Remove a persisted policy element
110
+ #
111
+ def delete(element)
112
+ if %w[user_attribute object_attribute].include?(element.pe_type)
113
+ element.outgoing(:in_association).each do |assoc|
114
+ assoc.del
115
+ end
116
+ end
117
+ element.del
118
+ end
119
+
120
+ ##
121
+ # Update a persisted policy element
122
+ #
123
+ def update(element, changes_hash)
124
+ element.neo_server.set_node_properties(element.neo_id, changes_hash)
125
+ end
126
+
127
+
128
+ ##
129
+ # Determine if the given node is in the policy machine or not.
130
+ def element_in_machine?(pe)
131
+ found_node = ::Neography::Node.find('nodes', 'unique_identifier', pe.unique_identifier)
132
+ !found_node.nil?
133
+ end
134
+
135
+ ##
136
+ # Add the given association to the policy map. If an association between user_attribute
137
+ # and object_attribute already exists, then replace it with that given in the arguments.
138
+ def add_association(user_attribute, operation_set, object_attribute, policy_machine_uuid)
139
+ remove_association(user_attribute, object_attribute, policy_machine_uuid)
140
+
141
+ # TODO: scope by policy machine uuid
142
+ unique_identifier = user_attribute.unique_identifier + object_attribute.unique_identifier
143
+ node_attrs = {
144
+ :unique_identifier => unique_identifier,
145
+ :policy_machine_uuid => policy_machine_uuid,
146
+ :user_attribute_unique_identifier => user_attribute.unique_identifier,
147
+ :object_attribute_unique_identifier => object_attribute.unique_identifier,
148
+ :operations => operation_set.map(&:unique_identifier).to_json,
149
+ }
150
+ persisted_assoc = ::Neography::Node.create(node_attrs)
151
+ persisted_assoc.add_to_index('associations', 'unique_identifier', unique_identifier)
152
+
153
+ [user_attribute, object_attribute, *operation_set].each do |element|
154
+ ::Neography::Relationship.create(:in_association, element, persisted_assoc)
155
+ end
156
+
157
+ true
158
+ end
159
+
160
+ ##
161
+ # Return all associations in which the given operation is included
162
+ # Returns an array of arrays. Each sub-array is of the form
163
+ # [user_attribute, operation_set, object_attribute]
164
+ #
165
+ def associations_with(operation)
166
+ operation.outgoing(:in_association).map do |association|
167
+ user_attribute = ::Neography::Node.find('nodes', 'unique_identifier', association.user_attribute_unique_identifier)
168
+ object_attribute = ::Neography::Node.find('nodes', 'unique_identifier', association.object_attribute_unique_identifier)
169
+
170
+ operation_set = Set.new
171
+ JSON.parse(association.operations).each do |op_unique_id|
172
+ op_node = ::Neography::Node.find('nodes', 'unique_identifier', op_unique_id)
173
+ operation_set << op_node
174
+ end
175
+
176
+ [user_attribute, operation_set, object_attribute]
177
+ end
178
+ end
179
+
180
+ ##
181
+ # Remove an existing association. Return true if the association was removed and false if
182
+ # it didn't exist in the first place.
183
+ def remove_association(user_attribute, object_attribute, policy_machine_uuid)
184
+ unique_identifier = user_attribute.unique_identifier + object_attribute.unique_identifier
185
+
186
+ begin
187
+ assoc_node = ::Neography::Node.find('associations', 'unique_identifier', unique_identifier)
188
+ return false unless assoc_node
189
+ assoc_node.del
190
+ true
191
+ rescue ::Neography::NotFoundException
192
+ false
193
+ end
194
+ end
195
+
196
+ ##
197
+ # Return array of all policy classes which contain the given object_attribute (or object).
198
+ # Return empty array if no such policy classes found.
199
+ def policy_classes_for_object_attribute(object_attribute)
200
+ find_all_of_type_policy_class.select do |pc|
201
+ connected?(object_attribute, pc)
202
+ end
203
+ end
204
+
205
+ ##
206
+ # Return array of all user attributes which contain the given user.
207
+ # Return empty array if no such user attributes are found.
208
+ def user_attributes_for_user(user)
209
+ #Don't use this kind of query plan in a for-production adapter.
210
+ find_all_of_type_user_attribute.select do |user_attribute|
211
+ connected?(user, user_attribute)
212
+ end
213
+ end
214
+
215
+ ##
216
+ # Execute the passed-in block transactionally: any error raised out of the block causes
217
+ # all the block's changes to be rolled back.
218
+ def transaction
219
+ raise NotImplementedError, "transactions are only available in neo4j 2.0 which #{self.class} is not compatible with"
220
+ end
221
+
222
+ private
223
+
224
+ # Raise argument error if argument is not suitable for consumption in
225
+ # public methods.
226
+ def assert_persisted_policy_element(arg)
227
+ raise(ArgumentError, "arg must be a Neography::Node; got #{arg.class.name}") unless arg.is_a?(::Neography::Node)
228
+ raise(ArgumentError, "arg must be persisted") unless element_in_machine?(arg)
229
+ end
230
+
231
+ # Neo4j client
232
+ def neo_connection
233
+ @neo_connection ||= ::Neography::Rest.new
234
+ end
235
+ end
236
+ end unless neography_unavailable