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