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,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