controlist 0.1.0

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.
@@ -0,0 +1,193 @@
1
+ module Controlist
2
+
3
+ class Interceptor
4
+
5
+ PROXY_CLASSES = {}
6
+
7
+ class << self
8
+
9
+ def hook
10
+ hook_read
11
+ hook_persistence
12
+ hook_attribute
13
+ end
14
+
15
+ # used by hook_attribute
16
+ def build_proxy(target)
17
+ klass = target.class
18
+ proxy_class = (PROXY_CLASSES[klass] ||= create_value_object_proxy_class klass)
19
+ proxy_class.new target
20
+ end
21
+
22
+ private
23
+
24
+ # Avoid ActiveModel::MissingAttributeError due to select(attributes) according to constrains
25
+ # #suppose attribute_proxy is :_val, value_object_proxy is :_value_object
26
+ # user = User.find 1
27
+ # user._val(:name)
28
+ # user._value_object.name
29
+ def hook_attribute
30
+ ActiveRecord::Persistence.class_eval %Q{
31
+ def #{Controlist.attribute_proxy}(attr)
32
+ self.#{Controlist.value_object_proxy}[attr]
33
+ end
34
+
35
+ def #{Controlist.value_object_proxy}
36
+ @#{Controlist.value_object_proxy} ||= Controlist::Interceptor.build_proxy self
37
+ end
38
+ }
39
+ end
40
+
41
+ def hook_persistence
42
+ if Controlist.is_activerecord3?
43
+ settings = {
44
+ create: :create,
45
+ update: :update,
46
+ delete: [:delete, :destroy]
47
+ }
48
+ else
49
+ settings = {
50
+ create: :_create_record,
51
+ update: :_update_record,
52
+ delete: [:delete, :destroy]
53
+ }
54
+ end
55
+ settings.each do |operation, methods|
56
+ Array(methods).each do |method|
57
+ ActiveRecord::Persistence.module_eval %Q{
58
+ def #{method}_with_controlist(*args)
59
+ permission_provider = Controlist.permission_provider
60
+ unless permission_provider.skip?
61
+ permission_package = permission_provider.get_permission_package
62
+ permissions = permission_package.list_#{operation}[self.class] if permission_package
63
+ if permissions.blank?
64
+ raise NoPermissionError
65
+ else
66
+ passed = false
67
+ matched_permission = nil
68
+ permissions.each do |permission|
69
+ if permission.match_for_persistence(self, Controlist::Permission::#{operation.upcase})
70
+ Controlist.logger.debug{"Controlist matched to \#{permission.is_allowed ? 'allow' : 'forbid'} \#{permission.inspect}"}
71
+ if permission.is_allowed
72
+ passed = true
73
+ end
74
+ matched_permission = permission
75
+ break
76
+ end
77
+ end
78
+ if passed
79
+ Controlist.logger.debug{"Controlist #{operation} checked: PASSED"}
80
+ else
81
+ Controlist.logger.debug{"Controlist #{operation} checked: FORBIDDEN"}
82
+ if matched_permission.nil?
83
+ raise NoPermissionError
84
+ else
85
+ raise PermissionForbidden.new "Forbidden by permission", matched_permission
86
+ end
87
+ end
88
+ end
89
+ end
90
+ #{method}_without_controlist(*args)
91
+ end
92
+ alias_method_chain :#{method}, :controlist unless method_defined? :#{method}_without_controlist
93
+ }
94
+ end
95
+ end
96
+ end
97
+
98
+ def hook_read
99
+ if Controlist.is_activerecord3?
100
+ ActiveRecord::QueryMethods.module_eval do
101
+ def where!(opts, *rest)
102
+ return if opts.blank?
103
+ self.where_values += build_where(opts, rest)
104
+ end
105
+ def _select!(*value)
106
+ self.select_values += Array.wrap(value)
107
+ end
108
+ def joins!(*args)
109
+ return if args.compact.blank?
110
+ args.flatten!
111
+ self.joins_values += args
112
+ end
113
+ end
114
+ ActiveRecord::IdentityMap.module_eval do
115
+ def self.enabled?
116
+ false
117
+ end
118
+ def self.enabled
119
+ false
120
+ end
121
+ end
122
+ else
123
+ ActiveRecord::Core::ClassMethods.module_eval do
124
+ #Bypass find_by_statement_cache, otherwise will use cached sql which may has wrong permissions
125
+ def find(*args)
126
+ super
127
+ end
128
+ def find_by(*args)
129
+ super
130
+ end
131
+ end
132
+ end
133
+ ActiveRecord::Relation.class_eval do
134
+ def build_arel_with_controlist
135
+ permission_provider = Controlist.permission_provider
136
+ if permission_provider.skip? || @controlist_processing
137
+ build_arel_without_controlist
138
+ else
139
+ if @controlist_done
140
+ raise Controlist::NotReuseableError.new("The relation has built a sql, you can't reuse it, or you can clone it before sql building", self)
141
+ else
142
+ @controlist_processing = true
143
+ permission_provider = Controlist.permission_provider
144
+ unless permission_provider.skip?
145
+ permission_package = permission_provider.get_permission_package
146
+ permissions = permission_package.list_read[@klass] if permission_package
147
+ if permissions.blank?
148
+ self.where!("1 != 1")
149
+ else
150
+ permissions.each do |permission|
151
+ permission.handle_for_read self
152
+ end
153
+ end
154
+ end
155
+ @controlist_processing = false
156
+ @controlist_done = true
157
+ build_arel_without_controlist
158
+ end
159
+ end
160
+ end
161
+ alias_method_chain :build_arel, :controlist unless method_defined? :build_arel_without_controlist
162
+ end
163
+ end
164
+
165
+ def create_value_object_proxy_class(klass)
166
+ attributes = klass.columns.map(&:name)
167
+ attributes.delete klass.primary_key
168
+ proxy_class = Class.new
169
+ code_block = ""
170
+ attributes.each do |attribute|
171
+ code_block += %Q{
172
+ def #{attribute}
173
+ @target.#{attribute} rescue nil
174
+ end
175
+ }
176
+ end
177
+ proxy_class.class_eval %Q{
178
+ def initialize(target)
179
+ @target = target
180
+ end
181
+ def [](attribute)
182
+ @target[attribute] rescue nil
183
+ end
184
+ #{code_block}
185
+ }
186
+ proxy_class
187
+ end
188
+
189
+ end
190
+
191
+ end
192
+
193
+ end
@@ -0,0 +1,31 @@
1
+ module Controlist
2
+ module Managers
3
+ class BaseManager
4
+
5
+ class << self
6
+
7
+ def get_permission_package
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def set_permission_package(package)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def skip?
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def open_skip
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def close_skip
24
+ raise NotImplementedError
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ module Controlist
2
+ module Managers
3
+ class ThreadBasedManager < BaseManager
4
+
5
+ class << self
6
+
7
+ def get_permission_package
8
+ Thread.current[:permission_package]
9
+ end
10
+
11
+ def set_permission_package(package)
12
+ Thread.current[:permission_package] = package
13
+ end
14
+
15
+ def skip?
16
+ Thread.current[:skip_controlist] == true
17
+ end
18
+
19
+ def open_skip
20
+ Thread.current[:skip_controlist] = true
21
+ end
22
+
23
+ def close_skip
24
+ Thread.current[:skip_controlist] = nil
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,209 @@
1
+ require 'controlist/permissions/operation'
2
+ require 'controlist/permissions/constrain'
3
+ require 'controlist/permissions/simple_constrain'
4
+ require 'controlist/permissions/advanced_constrain'
5
+ require 'controlist/permissions/ordered_package'
6
+
7
+ module Controlist
8
+ class Permission
9
+
10
+ # properties is hash with property and value pair, operation READ only need keys
11
+ attr_accessor :klass, :operations, :is_allowed, :constrains, :clause, :joins, :properties, :procs_read
12
+
13
+ def initialize(klass, operations, is_allowed = true, constrains=nil)
14
+ self.procs_read = []
15
+ self.klass = klass
16
+ unless operations.nil?
17
+ if operations.is_a? Array
18
+ self.operations = operations
19
+ else
20
+ self.operations = [operations]
21
+ end
22
+ end
23
+ self.is_allowed = is_allowed
24
+ self.joins = []
25
+ if self.operations.nil?
26
+ init_for_read constrains
27
+ init_for_persistence constrains
28
+ else
29
+ init_for_read constrains if self.operations.include? Controlist::Permissions::READ
30
+ if self.operations.include?(Controlist::Permissions::CREATE) ||
31
+ self.operations.include?(Controlist::Permissions::UPDATE) ||
32
+ self.operations.include?(Controlist::Permissions::DELETE)
33
+ init_for_persistence constrains
34
+ end
35
+ end
36
+ end
37
+
38
+ def apply(*properties)
39
+ self.properties = {id: nil}
40
+ properties.each do |property_pair|
41
+ if property_pair.is_a? Hash
42
+ self.properties.merge! property_pair
43
+ else
44
+ self.properties[property_pair] = nil
45
+ end
46
+ end
47
+ self
48
+ end
49
+
50
+ def handle_for_read(relation)
51
+ relation._select!(*self.properties.keys) unless self.properties.blank?
52
+ relation.joins!(*self.joins) if self.joins.size > 0
53
+ relation.where!("#{self.clause}") if self.clause
54
+ unless self.procs_read.blank?
55
+ # Only support ActiveRecord 4
56
+ merging_relation = self.klass.unscoped
57
+ self.procs_read.each do |proc|
58
+ merging_relation = proc.call(merging_relation)
59
+ end
60
+ ActiveRecord::Relation::Merger.new(relation, merging_relation).merge
61
+ end
62
+
63
+ end
64
+
65
+ def match_for_persistence(object, operation)
66
+ properties_matched = match_properties_for_persistence object, operation
67
+ properties_matched && match_constains_for_persistence(object, operation)
68
+ end
69
+
70
+ def match_properties_for_persistence(object, operation)
71
+ return true if operation == Controlist::Permissions::DELETE || self.properties.blank?
72
+ properties_matched = false
73
+ changes = object.changes
74
+ self.properties.each do |property, value|
75
+ change = changes[property]
76
+ if change && (value.nil? || Array(value).include?(change.last))
77
+ properties_matched = true
78
+ break
79
+ end
80
+ end
81
+ Controlist.logger.debug{"Controlist #{operation} properties checked: #{properties_matched}"}
82
+ properties_matched
83
+ end
84
+
85
+ def match_constains_for_persistence(object, operation)
86
+ if self.constrains.blank?
87
+ constrain_matched = true
88
+ else
89
+ constrain_matched = self.constrains.any? do |constrain|
90
+ if constrain.proc_persistence.is_a?(Proc) && constrain.proc_persistence.lambda?
91
+ inner_matched = constrain.proc_persistence.call object, operation
92
+ else
93
+ inner_matched = false
94
+ property = constrain.property
95
+ value = constrain.value
96
+ operator = constrain.operator
97
+ if constrain.relation.nil?
98
+ if object.persisted? && (changes = object.changes[property])
99
+ inner_matched = match_value(changes.first, value, operator)
100
+ else
101
+ inner_matched = match_value(object[property], value, operator)
102
+ end
103
+ else
104
+ relation_object = object.send(constrain.relation)
105
+ inner_matched = (relation_object && match_value(relation_object[property], value, operator))
106
+ end
107
+ end
108
+ inner_matched
109
+ end
110
+ end
111
+ Controlist.logger.debug{"Controlist #{operation} constrains checked: #{constrain_matched}"}
112
+ constrain_matched
113
+ end
114
+
115
+ private
116
+
117
+ def init_for_persistence(constrains)
118
+ return if constrains.nil?
119
+ if !(constrains.is_a?(Controlist::Permissions::Constrain) ||constrains.is_a?(Array))
120
+ raise ArgumentError.new("constrains has unknown type #{constrains.class}")
121
+ end
122
+ constrains = [constrains] if constrains.is_a? Controlist::Permissions::Constrain
123
+ constrains.compact!
124
+ constrains.each do |constrain|
125
+ raise "Persistence operation can't use constrain clause" unless constrain.clause.blank?
126
+ end
127
+ self.constrains = constrains
128
+ end
129
+
130
+ def match_value(left, right, operator)
131
+ if operator.nil?
132
+ left == right
133
+ else
134
+ left.send(operator.to_sym, right)
135
+ end
136
+ end
137
+
138
+ def init_for_read(constrains)
139
+ return if constrains.nil?
140
+ case constrains
141
+ when String
142
+ self.clause = constrains
143
+ when Array, Controlist::Permissions::Constrain
144
+ constrains = [constrains] if constrains.is_a? Controlist::Permissions::Constrain
145
+ self.constrains = constrains.compact
146
+ self.clause = build_clause
147
+ self.clause = "not (#{self.clause})" if self.is_allowed == false
148
+ else
149
+ raise ArgumentError.new("constrains has unknown type #{constrains.class}")
150
+ end
151
+ end
152
+
153
+ def build_clause
154
+ clause = ""
155
+ self.constrains.each do |constrain|
156
+ if constrain.proc_read.is_a?(Proc)
157
+ self.procs_read << constrain.proc_read
158
+ next
159
+ else
160
+ if !constrain.clause.nil?
161
+ part_clause = constrain.clause
162
+ else
163
+ table_name = append_joins constrain.relation if constrain.relation
164
+ table_name ||= (constrain.table_name || self.klass.table_name)
165
+ property = constrain.property
166
+ value = constrain.value
167
+ raise ArgumentError.new("property could not be nil") if property.blank?
168
+ raise ArgumentError.new("value could not be nil") if value.blank?
169
+ default_operator = '='
170
+ if value.is_a?(Proc) && value.lambda?
171
+ Controlist.skip{ value = value.call }
172
+ end
173
+ if value.is_a? Array
174
+ if value.first.is_a? String
175
+ value = "('" + value.join("','") + "')"
176
+ else
177
+ value = "(" + value.join(",") + ")"
178
+ end
179
+ default_operator = 'in'
180
+ else
181
+ if value.is_a? String
182
+ if value.upcase == 'NULL'
183
+ default_operator = 'is'
184
+ else
185
+ value = "'#{value}'"
186
+ end
187
+ end
188
+ end
189
+ operator = constrain.operator || default_operator
190
+ part_clause = "#{table_name}.#{property} #{operator} #{value}"
191
+ end
192
+ clause += " and " if clause.length > 0
193
+ clause += "(#{part_clause})"
194
+ end
195
+ end
196
+ clause
197
+ end
198
+
199
+ def append_joins(relation_name)
200
+ reflections = self.klass.reflections
201
+ # Rails 4.2 use string key, instead Rails 4.1 use symbol key
202
+ relation = reflections[relation_name.to_s] || reflections[relation_name.to_sym]
203
+ raise "Relation #{relation_name} Not found for class #{self.klass}!" if relation.nil?
204
+ self.joins << relation_name.to_sym
205
+ relation.table_name
206
+ end
207
+
208
+ end
209
+ end
@@ -0,0 +1,24 @@
1
+ module Controlist
2
+ module Permissions
3
+
4
+ class AdvancedConstrain < Constrain
5
+
6
+ def initialize(hash)
7
+ self.property = hash[:property]
8
+ self.value = hash[:value]
9
+ self.relation = hash[:relation]
10
+ self.table_name = hash[:table_name]
11
+ self.operator = hash[:operator]
12
+ self.clause = hash[:clause]
13
+ if Controlist.is_activerecord3? && (hash.has_key?(:proc_read) || hash.has_key?(:proc_persistence))
14
+ raise NotImplementedError, "Skip proc_read and proc_persistence, that features only be supported in ActiveRecord 4 or later"
15
+ else
16
+ self.proc_read = hash[:proc_read]
17
+ self.proc_persistence = hash[:proc_persistence]
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ module Controlist
2
+ module Permissions
3
+
4
+ class Constrain
5
+ attr_accessor :property, :value, :relation, :table_name, :operator, :clause, :proc_read, :proc_persistence
6
+ end
7
+
8
+ end
9
+ end
10
+
@@ -0,0 +1,32 @@
1
+ module Controlist
2
+ module Permissions
3
+
4
+ CREATE = :create
5
+ READ = :read
6
+ UPDATE = :update
7
+ DELETE = :delete
8
+
9
+ module_function
10
+
11
+ def is_persistence?(operation)
12
+ [CREATE, UPDATE, DELETE].include? operation.to_sym
13
+ end
14
+
15
+ def is_create?(operation)
16
+ CREATE == operation.to_sym
17
+ end
18
+
19
+ def is_read?(operation)
20
+ READ == operation.to_sym
21
+ end
22
+
23
+ def is_update?(operation)
24
+ UPDATE == operation.to_sym
25
+ end
26
+
27
+ def is_delete?(operation)
28
+ DELETE == operation.to_sym
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,56 @@
1
+ module Controlist
2
+ module Permissions
3
+
4
+ class OrderedPackage
5
+
6
+ attr_reader :list_create, :list_read, :list_update, :list_delete, :permissions
7
+
8
+ def initialize(*permissions)
9
+ permissions.compact!
10
+ @list_create = {}
11
+ @list_read = {}
12
+ @list_update = {}
13
+ @list_delete = {}
14
+ @permissions = permissions
15
+ @permissions.freeze # avoid bypassing add_permissions/remove_permissions
16
+ add_permissions *permissions
17
+ end
18
+
19
+ def add_permissions(*permissions)
20
+ @permissions += permissions
21
+ @permissions.freeze
22
+ permissions.each do |permission|
23
+ operations = permission.operations
24
+ add @list_create, permission if operations.nil? || operations.include?(CREATE)
25
+ add @list_read, permission if operations.nil? || operations.include?(READ)
26
+ add @list_update, permission if operations.nil? || operations.include?(UPDATE)
27
+ add @list_delete, permission if operations.nil? || operations.include?(DELETE)
28
+ end
29
+ end
30
+
31
+ def remove_permissions(*permissions)
32
+ @permissions -= permissions
33
+ @permissions.freeze
34
+ permissions.each do |permission|
35
+ operations = permission.operations
36
+ remove @list_create, permission if operations.nil? || operations.include?(CREATE)
37
+ remove @list_read, permission if operations.nil? || operations.include?(READ)
38
+ remove @list_update, permission if operations.nil? || operations.include?(UPDATE)
39
+ remove @list_delete, permission if operations.nil? || operations.include?(DELETE)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def add(list, permission)
46
+ (list[permission.klass] ||= []) << permission
47
+ end
48
+
49
+ def remove(list, permission)
50
+ (list[permission.klass] ||= []).delete permission
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,16 @@
1
+ module Controlist
2
+ module Permissions
3
+
4
+ class SimpleConstrain < Constrain
5
+
6
+ def initialize(property, value, hash={})
7
+ self.property = property.to_s
8
+ self.value = value
9
+ self.relation = hash[:relation]
10
+ self.table_name = hash[:table_name]
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Controlist
2
+ VERSION = "0.1.0"
3
+ end
data/lib/controlist.rb ADDED
@@ -0,0 +1,32 @@
1
+ require "controlist/version"
2
+ require "controlist/errors"
3
+ require "controlist/permission"
4
+ require "controlist/interceptor"
5
+ require "controlist/managers/base_manager"
6
+
7
+ module Controlist
8
+
9
+ class << self
10
+
11
+ attr_accessor :permission_provider, :attribute_proxy, :value_object_proxy, :logger
12
+
13
+ def initialize(permission_provider, config={})
14
+ @permission_provider = permission_provider
15
+ @attribute_proxy = config[:attribute_proxy] || "_val"
16
+ @value_object_proxy = config[:value_object_proxy] || "_value_object"
17
+ @logger = config[:logger] || Logger.new(STDOUT)
18
+ Interceptor.hook
19
+ end
20
+
21
+ def skip
22
+ @permission_provider.open_skip
23
+ yield
24
+ @permission_provider.close_skip
25
+ end
26
+
27
+ def is_activerecord3?
28
+ ActiveRecord::VERSION::MAJOR == 3
29
+ end
30
+ end
31
+
32
+ end