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.
- data/CONTRIBUTING.md +35 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.md +98 -0
- data/lib/generators/policy_machine/policy_machine_generator.rb +13 -0
- data/lib/generators/policy_machine/templates/migration.rb +40 -0
- data/lib/policy_machine.rb +236 -0
- data/lib/policy_machine/association.rb +73 -0
- data/lib/policy_machine/policy_element.rb +269 -0
- data/lib/policy_machine/version.rb +3 -0
- data/lib/policy_machine_storage_adapters/active_record.rb +306 -0
- data/lib/policy_machine_storage_adapters/in_memory.rb +266 -0
- data/lib/policy_machine_storage_adapters/neography.rb +236 -0
- data/lib/policy_machine_storage_adapters/template.rb +169 -0
- data/lib/tasks/policy_machine_tasks.rake +4 -0
- data/policy_machine.gemspec +23 -0
- data/spec/policy_machine/association_spec.rb +61 -0
- data/spec/policy_machine/policy_element_spec.rb +20 -0
- data/spec/policy_machine_spec.rb +7 -0
- data/spec/policy_machine_storage_adapters/active_record_spec.rb +54 -0
- data/spec/policy_machine_storage_adapters/in_memory_spec.rb +13 -0
- data/spec/policy_machine_storage_adapters/neography_spec.rb +42 -0
- data/spec/policy_machine_storage_adapters/template_spec.rb +6 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/neography_helpers.rb +39 -0
- data/spec/support/policy_machine_helpers.rb +22 -0
- data/spec/support/shared_examples_policy_machine_spec.rb +697 -0
- data/spec/support/shared_examples_policy_machine_storage_adapter_spec.rb +278 -0
- data/spec/support/shared_examples_storage_adapter_public_methods.rb +20 -0
- data/spec/support/storage_adapter_helpers.rb +7 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +65 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +42 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/db/migrate/20131015214828_generate_policy_machine.rb +40 -0
- data/test/dummy/db/migrate/20131021221759_add_color_to_policy_element.rb +5 -0
- data/test/dummy/db/schema.rb +57 -0
- data/test/dummy/lib/assets/.gitkeep +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/policy_machine_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- 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,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
|