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
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,35 @@
1
+ # Contributing
2
+
3
+ If you find a bug:
4
+
5
+ * Check the "GitHub issue tracker" to see if anyone else has reported issue.
6
+ * If you don't see anything, create an issue with information on how to reproduce it.
7
+
8
+ If you want to contribute an enhancement or a fix:
9
+
10
+ * Fork the project on GitHub.
11
+ * bundle install
12
+ * Make your changes with tests.
13
+ * Run all automated tests to see if your change broke anything or is providing anything less than 100% code coverage (see below).
14
+ * Commit the changes without making changes to any other files that aren't related to your enhancement or fix.
15
+ * Send a pull request.
16
+
17
+ ## Running Automated Tests
18
+
19
+ Run all rspec with:
20
+
21
+ ```
22
+ [bundle exec] rspec
23
+ ```
24
+
25
+ Simplecov code coverage is generated automatically. Any changes you make to this repository should
26
+ ensure that code coverage remains at 100%. **No pull request will be merged unless proof is given
27
+ (i.e. a PR comment) that code coverage remains at 100% in the PR branch.**
28
+
29
+ ## Making Your Own Policy Machine Storage Adapter
30
+
31
+ A template storage adapter is provided in `lib/policy_machine_storage_adapters/template.rb`. Copy this
32
+ storage adapter as a starting point for making your own; implement all methods contained therein.
33
+
34
+ To test your storage adapter, adapt the tests in either `spec/policy_machine_storage_adapters/in_memory_spec.rb` or
35
+ `spec/policy_machine_storage_adapters/neography_spec.rb`.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ policy_machine
2
+ ==============
3
+
4
+ A ruby implementation of the Policy Machine authorization formalism. You can find the NIST specification for Policy
5
+ Machines [here](http://csrc.nist.gov/pm/documents/pm_report-rev-x_final.pdf).
6
+
7
+ Note that prohibitions, obligations and multiple policy classes have not yet been implemented. These aspects of the Policy Machine
8
+ will be included in future versions of this gem.
9
+
10
+ # Installation
11
+
12
+ Add the following to your Gemfile:
13
+ ```
14
+ gem 'policy_machine'
15
+ ```
16
+
17
+ # Usage
18
+ ```
19
+ require 'policy_machine'
20
+ require 'policy_machine_storage_adapters/in_memory'
21
+
22
+ policy_machine = PolicyMachine.new('my_policy_machine', ::PolicyMachineStorageAdapter::InMemory)
23
+
24
+ # This PM is taken from the policy machine spec at http://csrc.nist.gov/pm/documents/pm_report-rev-x_final.pdf,
25
+ # Figure 4. (pg. 19)
26
+
27
+ # Users
28
+ u1 = policy_machine.create_user('u1')
29
+ u2 = policy_machine.create_user('u2')
30
+ u3 = policy_machine.create_user('u3')
31
+
32
+ # Objects
33
+ o1 = policy_machine.create_object('o1')
34
+ o2 = policy_machine.create_object('o2')
35
+ o3 = policy_machine.create_object('o3')
36
+
37
+ # User Attributes
38
+ group1 = policy_machine.create_user_attribute('Group1')
39
+ group2 = policy_machine.create_user_attribute('Group2')
40
+ division = policy_machine.create_user_attribute('Division')
41
+
42
+ # Object Attributes
43
+ project1 = policy_machine.create_object_attribute('Project1')
44
+ project2 = policy_machine.create_object_attribute('Project2')
45
+ projects = policy_machine.create_object_attribute('Projects')
46
+
47
+ # Operations
48
+ r = policy_machine.create_operation('read')
49
+ w = policy_machine.create_operation('write')
50
+
51
+ # Assignments
52
+ policy_machine.add_assignment(u1, group1)
53
+ policy_machine.add_assignment(u2, group2)
54
+ policy_machine.add_assignment(u3, division)
55
+ policy_machine.add_assignment(group1, division)
56
+ policy_machine.add_assignment(group2, division)
57
+ policy_machine.add_assignment(o1, project1)
58
+ policy_machine.add_assignment(o2, project1)
59
+ policy_machine.add_assignment(o3, project2)
60
+ policy_machine.add_assignment(project1, projects)
61
+ policy_machine.add_assignment(project2, projects)
62
+
63
+ # Associations
64
+ policy_machine.add_association(group1, Set.new([w]), project1)
65
+ policy_machine.add_association(group2, Set.new([w]), project2)
66
+ policy_machine.add_association(division, Set.new([r]), projects)
67
+
68
+ # List all privileges encoded in the policy machine
69
+ policy_machine.privileges
70
+
71
+ # Returns true
72
+ policy_machine.is_privilege?(u1, w, o1)
73
+
74
+ # Returns false
75
+ policy_machine.is_privilege?(u3, w, o3)
76
+ ```
77
+
78
+ # Storage Adapters
79
+
80
+ Note that the Policy Machine in the above example stores policy elements in memory. Other persistent
81
+ storage options are available in `lib/policy_machine_storage_adapters`.
82
+
83
+ *Neography*
84
+
85
+ The Neography storage adapter uses the neo4j graph database, which must be installed separately,
86
+ and `gem 'neography'`. This should not be used in production since the interface is slow.
87
+
88
+ *ActiveRecord*
89
+
90
+ The ActiveRecord storage adapter talks to your existing MySQL database via your preconfigured
91
+ ActiveRecord. You'll need to run `rails generate policy_machine migration` to add the necessary
92
+ tables to your database.
93
+
94
+ If you'd like to make your own storage adapter, see See [CONTRIBUTING.md](CONTRIBUTING.md).
95
+
96
+ # Contributing
97
+
98
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,13 @@
1
+ require 'rails/generators/active_record/migration/migration_generator'
2
+
3
+ class PolicyMachineGenerator < ::ActiveRecord::Generators::MigrationGenerator
4
+ desc "Create a migration to store Policy Machine elements in your database"
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def initialize(*args)
9
+ args[0] = ['generate_policy_machine']
10
+ super(*args)
11
+ end
12
+
13
+ end
@@ -0,0 +1,40 @@
1
+ class GeneratePolicyMachine < ActiveRecord::Migration
2
+ def change
3
+
4
+ create_table :policy_elements do |t|
5
+ t.string :unique_identifier, null: false
6
+ t.string :policy_machine_uuid
7
+ t.string :type, null: false
8
+ t.text :extra_attributes
9
+ end
10
+ add_index :policy_elements, [:unique_identifier], unique: true
11
+ add_index :policy_elements, [:type]
12
+
13
+ create_table :policy_element_associations do |t|
14
+ t.integer :user_attribute_id, null: false
15
+ t.integer :object_attribute_id, null: false
16
+ end
17
+ add_index :policy_element_associations, [:user_attribute_id, :object_attribute_id], name: 'index_pe_assocs_on_ua_and_oa'
18
+
19
+ create_table :transitive_closure, id: false do |t|
20
+ t.integer :ancestor_id, null: false
21
+ t.integer :descendant_id, null: false
22
+ end
23
+ add_index :transitive_closure, [:ancestor_id, :descendant_id], unique: true
24
+ add_index :transitive_closure, [:descendant_id]
25
+
26
+ create_table :assignments do |t|
27
+ t.integer :parent_id, null: false
28
+ t.integer :child_id, null: false
29
+ end
30
+ add_index :assignments, [:parent_id, :child_id], unique: true
31
+ add_index :assignments, [:child_id]
32
+
33
+ create_table :operations_policy_element_associations, id: false do |t|
34
+ t.integer :policy_element_association_id, null: false
35
+ t.integer :operation_id, null: false
36
+ end
37
+ add_index :operations_policy_element_associations, [:policy_element_association_id, :operation_id], unique: true, name: 'index_pe_assoc_os_on_assoc_and_o'
38
+
39
+ end
40
+ end
@@ -0,0 +1,236 @@
1
+ require 'policy_machine/policy_element'
2
+ require 'policy_machine/association'
3
+ require 'securerandom'
4
+ require 'active_support/inflector'
5
+ require 'set'
6
+
7
+ # require all adapters
8
+ Dir.glob(File.dirname(File.absolute_path(__FILE__)) + '/policy_machine_storage_adapters/*.rb').each{ |f| require f }
9
+
10
+ class PolicyMachine
11
+ POLICY_ELEMENT_TYPES = %w(user user_attribute object object_attribute operation policy_class)
12
+
13
+ attr_accessor :name
14
+ attr_reader :uuid
15
+ attr_reader :policy_machine_storage_adapter
16
+
17
+ def initialize(options = {})
18
+ @name = (options[:name] || options['name'] || 'default_policy_machine').to_s.strip
19
+ @uuid = (options[:uuid] || options['uuid'] || SecureRandom.uuid).to_s.strip
20
+ policy_machine_storage_adapter_class = options[:storage_adapter] || options['storage_adapter'] || ::PolicyMachineStorageAdapter::InMemory
21
+ @policy_machine_storage_adapter = policy_machine_storage_adapter_class.new
22
+
23
+ raise(ArgumentError, "uuid cannot be blank") if @uuid.empty?
24
+ end
25
+
26
+ ##
27
+ # Persist an assignment in this policy machine.
28
+ # An assignment is a binary relation between two existing policy elements.
29
+ # Some policy element types cannot be assigned to other types. See the NIST
30
+ # spec for details.
31
+ #
32
+ def add_assignment(src_policy_element, dst_policy_element)
33
+ assert_policy_element_in_machine(src_policy_element)
34
+ assert_policy_element_in_machine(dst_policy_element)
35
+
36
+ src_policy_element.assign_to(dst_policy_element)
37
+ end
38
+
39
+ ##
40
+ # Remove an assignment in this policy machine.
41
+ #
42
+ def remove_assignment(src_policy_element, dst_policy_element)
43
+ assert_policy_element_in_machine(src_policy_element)
44
+ assert_policy_element_in_machine(dst_policy_element)
45
+
46
+ src_policy_element.unassign(dst_policy_element)
47
+ end
48
+
49
+ ##
50
+ # Add an association between a user_attribute, an operation_set and an object_attribute
51
+ # in this policy machine.
52
+ #
53
+ def add_association(user_attribute_pe, operation_set, object_attribute_pe)
54
+ assert_policy_element_in_machine(user_attribute_pe)
55
+ operation_set.each{ |op| assert_policy_element_in_machine(op) }
56
+ assert_policy_element_in_machine(object_attribute_pe)
57
+
58
+ PM::Association.create(user_attribute_pe, operation_set, object_attribute_pe, @uuid, @policy_machine_storage_adapter)
59
+ end
60
+
61
+ ##
62
+ # Can we derive a privilege of the form (u, op, o) from this policy machine?
63
+ # user_or_attribute is a user or user_attribute.
64
+ # operation is an operation.
65
+ # object_or_attribute is an object or object attribute.
66
+ #
67
+ # TODO: add option to ignore policy classes to allow consumer to speed up this method.
68
+ def is_privilege?(user_or_attribute, operation, object_or_attribute, options = {})
69
+ unless user_or_attribute.is_a?(PM::User) || user_or_attribute.is_a?(PM::UserAttribute)
70
+ raise(ArgumentError, "user_attribute_pe must be a User or UserAttribute.")
71
+ end
72
+
73
+ unless operation.is_a?(PM::Operation)
74
+ raise(ArgumentError, "operation must be an Operation.")
75
+ end
76
+
77
+ unless object_or_attribute.is_a?(PM::Object) || object_or_attribute.is_a?(PM::ObjectAttribute)
78
+ raise(ArgumentError, "object_or_attribute must either be an Object or ObjectAttribute.")
79
+ end
80
+
81
+ # Try to get associations to check from options
82
+ associations = options[:associations] || options['associations']
83
+ if associations
84
+ raise(ArgumentError, "expected options[:associations] to be an Array; got #{associations.class}") unless associations.is_a?(Array)
85
+ raise(ArgumentError, "options[:associations] cannot be empty") if associations.empty?
86
+ raise(ArgumentError, "expected each element of options[:associations] to be a PM::Association") unless associations.all?{|a| a.is_a?(PM::Association)}
87
+
88
+ associations.keep_if{ |assoc| assoc.includes_operation?(operation) }
89
+ return false if associations.empty?
90
+ else
91
+ associations = operation.associations
92
+ end
93
+
94
+ # Is a privilege iff options[:in_user_attribute] is involved (given options[:in_user_attribute] is not nil)
95
+ in_user_attribute = options[:in_user_attribute] || options['in_user_attribute']
96
+ if in_user_attribute
97
+ unless in_user_attribute.is_a?(PM::UserAttribute)
98
+ raise(ArgumentError, "expected options[:in_user_attribute] to be a PM::UserAttribute; got #{in_user_attribute.class}")
99
+ end
100
+ if user_or_attribute.connected?(in_user_attribute)
101
+ user_or_attribute = in_user_attribute
102
+ else
103
+ return false
104
+ end
105
+ end
106
+
107
+ # Is a privilege iff options[:in_object_attribute] is involved (given options[:in_object_attribute] is not nil)
108
+ in_object_attribute = options[:in_object_attribute] || options['in_object_attribute']
109
+ if in_object_attribute
110
+ unless in_object_attribute.is_a?(PM::ObjectAttribute)
111
+ raise(ArgumentError, "expected options[:in_object_attribute] to be a PM::ObjectAttribute; got #{in_object_attribute.class}")
112
+ end
113
+ if object_or_attribute.connected?(in_object_attribute)
114
+ object_or_attribute = in_object_attribute
115
+ else
116
+ return false
117
+ end
118
+ end
119
+
120
+ policy_classes_containing_object = object_or_attribute.policy_classes
121
+ if policy_classes_containing_object.empty?
122
+ is_privilege_single_policy_class(user_or_attribute, object_or_attribute, associations)
123
+ else
124
+ is_privilege_multiple_policy_classes(user_or_attribute, object_or_attribute, associations, policy_classes_containing_object)
125
+ end
126
+ end
127
+
128
+ ##
129
+ # Returns an array of all privileges encoded in this
130
+ # policy machine. Each privilege is of the form:
131
+ # [PM::User, PM::Operation, PM::Object]
132
+ #
133
+ # TODO: might make privilege a class of its own
134
+ def privileges
135
+ privileges = []
136
+
137
+ users.each do |user|
138
+ operations.each do |operation|
139
+ objects.each do |object|
140
+ if is_privilege?(user, operation, object)
141
+ privileges << [user, operation, object]
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ privileges
148
+ end
149
+
150
+ ##
151
+ # Returns an array of all user_attributes a PM::User is assigned to,
152
+ # directly or indirectly.
153
+ def list_user_attributes(user)
154
+ unless user.is_a?(PM::User)
155
+ raise(ArgumentError, "Expected a PM::User, got a #{user.class}")
156
+ end
157
+ assert_policy_element_in_machine(user)
158
+ user.user_attributes(@policy_machine_storage_adapter)
159
+ end
160
+
161
+ POLICY_ELEMENT_TYPES.each do |pe_type|
162
+ pm_class = "PM::#{pe_type.camelize}".constantize
163
+
164
+ ##
165
+ # Define a create method for each policy element type, as in create_user
166
+ # Each method takes one argument, the unique_identifier of the policy element.
167
+ #
168
+ define_method("create_#{pe_type}") do |unique_identifier, extra_attributes = {}|
169
+ # when creating a policy element, we provide a unique_identifier, the uuid of this policy machine
170
+ # and a policy machine storage adapter to allow us to persist the policy element.
171
+ pm_class.send(:create, unique_identifier, @uuid, @policy_machine_storage_adapter, extra_attributes)
172
+ end
173
+
174
+ ##
175
+ # Define an "all" method for each policy element type, as in .users or .object_attributes
176
+ # This will return all persisted of the elements of this type. If an options hash is passed
177
+ # then only elements that match all specified attributes will be returned.
178
+ #
179
+ define_method(pe_type.pluralize) do |options = {}|
180
+ # TODO: We might want to scope by the uuid of this policy machine in the request to the persistent store, rather than
181
+ # here, after records have already been retrieved.
182
+ # TODO: When the policy machine raises a NoMethoError, we should log a nice message
183
+ # saying that the underlying policy element class doesn't implement 'all'. Do
184
+ # it when we have a logger, though.
185
+ all_found = pm_class.send(:all, @policy_machine_storage_adapter, options)
186
+ all_found.select{ |pe| pe.policy_machine_uuid == uuid }
187
+ end
188
+ end
189
+
190
+ ##
191
+ # Execute the passed-in block transactionally: any error raised out of the block causes
192
+ # all the block's changes to be rolled back.
193
+ # TODO: Possibly rescue NotImplementError and warn.
194
+ def transaction(&block)
195
+ policy_machine_storage_adapter.transaction(&block)
196
+ end
197
+
198
+ private
199
+
200
+ # Raise unless the argument is a policy element.
201
+ def assert_policy_element_in_machine(arg_pe)
202
+ unless arg_pe.is_a?(PM::PolicyElement)
203
+ raise(ArgumentError, "arg must each be a kind of PolicyElement; got #{arg_pe.class.name} instead")
204
+ end
205
+ unless arg_pe.policy_machine_uuid == self.uuid
206
+ raise(ArgumentError, "#{arg_pe.unique_identifier} is not in policy machine with uuid #{self.uuid}")
207
+ end
208
+ end
209
+
210
+ # According to the NIST spec: "the triple (u, op, o) is a privilege, iff there
211
+ # exists an association (ua, ops, oa), such that user u→+ua, op ∈ ops, and o→*oa."
212
+ # Note: this method assumes that the caller has already checked that the given operation is in the operation_set
213
+ # for all associations provided.
214
+ def is_privilege_single_policy_class(user_or_attribute, object_or_attribute, associations)
215
+ # does there exist an association (ua, ops, oa), such that user u→+ua, op ∈ ops, and o→*oa?
216
+ associations.any? do |assoc|
217
+ user_or_attribute.connected?(assoc.user_attribute) && object_or_attribute.connected?(assoc.object_attribute)
218
+ end
219
+ end
220
+
221
+ # According to the NIST spec: "In multiple policy class situations, the triple (u, op, o) is a PM privilege, iff for
222
+ # each policy class pcl that contains o, there exists an association (uai, opsj, oak),
223
+ # such that user u→+uai, op ∈ opsj, o→*oak, and oak→+pcl."
224
+ # Note: this method assumes that the caller has already checked that the given operation is in the operation_set
225
+ # for all associations provided.
226
+ def is_privilege_multiple_policy_classes(user_or_attribute, object_or_attribute, associations, policy_classes_containing_object)
227
+ policy_classes_containing_object.all? do |pc|
228
+ associations.any? do |assoc|
229
+ user_or_attribute.connected?(assoc.user_attribute) &&
230
+ object_or_attribute.connected?(assoc.object_attribute) &&
231
+ assoc.object_attribute.connected?(pc)
232
+ end
233
+ end
234
+ end
235
+
236
+ end