policy_machine 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|