platanus 0.0.25 → 0.0.26
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/Gemfile +2 -0
- data/lib/platanus/canned2.rb +328 -0
- data/lib/platanus/version.rb +1 -1
- data/spec/canned2_spec.rb +83 -0
- metadata +5 -4
- data/spec/canned_spec.rb +0 -5
data/Gemfile
CHANGED
@@ -0,0 +1,328 @@
|
|
1
|
+
# canned2.rb : User profiling and authorization.
|
2
|
+
#
|
3
|
+
# Copyright October 2012, Ignacio Baixas +mailto:ignacio@platan.us+.
|
4
|
+
|
5
|
+
module Platanus
|
6
|
+
|
7
|
+
# User profiling and authorization module
|
8
|
+
module Canned2
|
9
|
+
|
10
|
+
class Interrupt < Exception; end
|
11
|
+
class Error < StandardError; end
|
12
|
+
class AuthError < Error; end
|
13
|
+
class SetupError < Error; end
|
14
|
+
|
15
|
+
# Controller extension, include this in the the base application
|
16
|
+
# controller and use the canned_setup method seal it.
|
17
|
+
module Controller
|
18
|
+
|
19
|
+
def self.included(klass)
|
20
|
+
class << klass
|
21
|
+
# Excluded actions and callbacks are defined in per class basis
|
22
|
+
attr_accessor :brk_excluded
|
23
|
+
attr_accessor :brk_before
|
24
|
+
end
|
25
|
+
klass.extend ClassMethods
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
|
30
|
+
## Setups the controller user profile definitions and profile provider block (or proc)
|
31
|
+
#
|
32
|
+
# The passed method or block must return a list of profiles to be validated
|
33
|
+
# by the definition.
|
34
|
+
#
|
35
|
+
# @param [Definition] _def Profile definitions
|
36
|
+
# @param [Symbol] _provider Profile provider method name
|
37
|
+
# @param [Block] _block Profile provider block
|
38
|
+
#
|
39
|
+
def canned_setup(_def, _provider=nil, &_block)
|
40
|
+
self.before_filter do
|
41
|
+
|
42
|
+
# no auth if action is excluded
|
43
|
+
next if self.class.brk_excluded == :all
|
44
|
+
next if !self.class.brk_excluded.nil? and self.class.brk_excluded.include? params[:action].to_sym
|
45
|
+
|
46
|
+
# call initializer block
|
47
|
+
profiles = if _provider.nil? then self.instance_eval(&_block) else self.send(_provider) end
|
48
|
+
raise AuthError if profiles.nil?
|
49
|
+
profiles = [profiles] unless profiles.is_a? Array
|
50
|
+
|
51
|
+
# call resource loader
|
52
|
+
brk_before = self.class.brk_before
|
53
|
+
unless brk_before.nil?
|
54
|
+
if brk_before.is_a? Symbol; self.send(brk_before)
|
55
|
+
else self.instance_eval &(brk_before) end
|
56
|
+
end
|
57
|
+
|
58
|
+
# execute authentication
|
59
|
+
# TODO: Add forbidden begin - rescue
|
60
|
+
result = profiles.collect do |profile|
|
61
|
+
_def.can?(self, profile, params[:controller]) or
|
62
|
+
_def.can?(self, profile, params[:controller] + '#' + params[:action])
|
63
|
+
end
|
64
|
+
raise AuthError unless result.any?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
## Removes protection for all controller actions.
|
69
|
+
def uncan_all()
|
70
|
+
self.brk_excluded = :all
|
71
|
+
end
|
72
|
+
|
73
|
+
## Removes protection for the especified controller actions.
|
74
|
+
#
|
75
|
+
# @param [splat] _excluded List of actions to be excluded.
|
76
|
+
#
|
77
|
+
def uncanned(*_excluded)
|
78
|
+
self.brk_excluded ||= []
|
79
|
+
self.brk_excluded.push(*_excluded)
|
80
|
+
end
|
81
|
+
|
82
|
+
## Specifies a block or method to be called before tests are ran.
|
83
|
+
#
|
84
|
+
# **IMPORTANT** Resources loaded here are avaliable to tests.
|
85
|
+
#
|
86
|
+
def before_auth(_callback=nil, &pblock)
|
87
|
+
self.brk_before = (_callback || pblock)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
## Holds all rules associated to a single user profile.
|
93
|
+
#
|
94
|
+
# This class describes the avaliable DSL when defining a new profile.
|
95
|
+
# TODO: example
|
96
|
+
class Profile
|
97
|
+
|
98
|
+
attr_reader :rules
|
99
|
+
attr_reader :def_matcher
|
100
|
+
attr_reader :def_resource
|
101
|
+
|
102
|
+
# The initializer takes another profile as rules base.
|
103
|
+
def initialize(_base, _def_matcher, _def_resource)
|
104
|
+
@rules = Hash.new { |h, k| h[k] = [] }
|
105
|
+
_base.rules.each { |k, tests| @rules[k] = tests.clone } unless _base.nil?
|
106
|
+
raise Error.new 'Must provide a default test' if _def_matcher.nil?
|
107
|
+
@def_matcher = _def_matcher
|
108
|
+
@def_resource = _def_resource
|
109
|
+
end
|
110
|
+
|
111
|
+
## Adds an "allowance" rule
|
112
|
+
def allow(_action, _upon=nil, &_block)
|
113
|
+
@rules[_action] << (_upon || _block)
|
114
|
+
end
|
115
|
+
|
116
|
+
## Adds a "forbidden" rule
|
117
|
+
def forbid(_action)
|
118
|
+
# TODO
|
119
|
+
end
|
120
|
+
|
121
|
+
## Clear all rules related to an action
|
122
|
+
def clear(_action)
|
123
|
+
@rules[_action] = []
|
124
|
+
end
|
125
|
+
|
126
|
+
## SHORT HAND METHODS
|
127
|
+
|
128
|
+
def upon(_expr=nil, &_block)
|
129
|
+
Proc.new { upon(_expr, &_block) }
|
130
|
+
end
|
131
|
+
|
132
|
+
def upon_one(_expr, &_block)
|
133
|
+
Proc.new { upon_one(_expr, &_block) }
|
134
|
+
end
|
135
|
+
|
136
|
+
def upon_all(_expr, &_block)
|
137
|
+
Proc.new { upon_all(_expr, &_block) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
## Rule block context
|
142
|
+
class RuleContext
|
143
|
+
|
144
|
+
def initialize(_ctx, _tests, _def_matcher, _def_resource)
|
145
|
+
@ctx = _ctx
|
146
|
+
@tests = _tests
|
147
|
+
@def_matcher = _def_matcher
|
148
|
+
@def_resource = UponContext.load_value_for(@ctx, _def_resource)
|
149
|
+
@passed = nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def passed?; @passed end
|
153
|
+
|
154
|
+
def upon(_res=nil, &_block)
|
155
|
+
return if @passed == false
|
156
|
+
res = if _res.nil? then @def_resource else UponContext.load_value_for(@ctx, _res) end
|
157
|
+
@passed = UponContext.new(res, @ctx, @tests, @def_matcher).instance_eval(&_block)
|
158
|
+
end
|
159
|
+
|
160
|
+
def upon_one(_res, &_block)
|
161
|
+
return if @passed == false
|
162
|
+
coll = if _res.nil? then @def_resource else UponContext.load_value_for(@ctx, _res) end
|
163
|
+
# TODO: Check coll type
|
164
|
+
@passed = coll.any? { |res| UponContext.new(res, @ctx, @tests, @def_matcher).instance_eval &_block }
|
165
|
+
end
|
166
|
+
|
167
|
+
def upon_all(_res, &_block)
|
168
|
+
return if @passed == false
|
169
|
+
coll = if _res.nil? then @def_resource else UponContext.load_value_for(@ctx, _res) end
|
170
|
+
# TODO: Check coll type
|
171
|
+
@passed = coll.all? { |res| UponContext.new(res, @ctx, @tests, @def_matcher).instance_eval &_block }
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
## Upon block context.
|
177
|
+
# allows '' do
|
178
|
+
# upon(:user_data) { matches(:site_id, using: :equals_int) or matches(:section_id) and passes(:is_owner) }
|
179
|
+
# upon { matches('current_user.site_id', with: :site_id) or matches(:section_id) }
|
180
|
+
# upon(:user) { matches(:site_id) or matches(:section_id) and passes(:test) or holds('user.is_active?') }
|
181
|
+
# upon { holds('@raffle.id == current_user.id') }
|
182
|
+
# end
|
183
|
+
class UponContext
|
184
|
+
|
185
|
+
def self.load_value_for(_ctx, _key_or_expr)
|
186
|
+
return _ctx if _key_or_expr.nil?
|
187
|
+
return _ctx[_key_or_expr] if _ctx.is_a? Hash
|
188
|
+
return _ctx.send(_key_or_expr) if _key_or_expr.is_a? Symbol
|
189
|
+
return _ctx.instance_eval(_key_or_expr)
|
190
|
+
end
|
191
|
+
|
192
|
+
def initialize(_res, _ctx, _tests, _def_matcher)
|
193
|
+
@res = _res
|
194
|
+
@ctx = _ctx
|
195
|
+
@tests = _tests
|
196
|
+
@def_matcher = _def_matcher
|
197
|
+
end
|
198
|
+
|
199
|
+
## Tests for a match between one of the request's parameters and a resource expression.
|
200
|
+
#
|
201
|
+
# **IMPORTANT** if no resource is provided the current controller instance is used instead.
|
202
|
+
#
|
203
|
+
# @param [Symbol] _what parameter name.
|
204
|
+
# @param [Symbol] :using matcher (:equals|:equals_int|:higher_than|:lower_than),
|
205
|
+
# uses profile default matcher if not provided.
|
206
|
+
# @param [Symbol|String] :on key or expression used to retrieve
|
207
|
+
# the matching value for current resource, if not given then _what is used.
|
208
|
+
# @param [Mixed] :value if given, this value is matched against parameter instead of resource's.
|
209
|
+
#
|
210
|
+
def matches(_what, _options={})
|
211
|
+
matcher = _options.fetch(:using, @def_matcher)
|
212
|
+
|
213
|
+
param = @ctx.params[_what]
|
214
|
+
return (matcher == :nil) if param.nil? # :nil matcher
|
215
|
+
|
216
|
+
if _options.has_key? :value
|
217
|
+
user_value = _options[:value]
|
218
|
+
else
|
219
|
+
user_value = self.class.load_value_for(@res, _options.fetch(:on, _what))
|
220
|
+
return false if user_value.nil?
|
221
|
+
return true if user_value == :wildcard
|
222
|
+
end
|
223
|
+
|
224
|
+
case matcher
|
225
|
+
when :equals; user_value == param
|
226
|
+
when :equals_int; user_value.to_i == param.to_i
|
227
|
+
when :higher_than; param > user_value
|
228
|
+
when :lower_than; param < user_value
|
229
|
+
else
|
230
|
+
# TODO: use custom matcher.
|
231
|
+
false
|
232
|
+
end
|
233
|
+
end
|
234
|
+
alias :match :matches
|
235
|
+
|
236
|
+
## Test whether the current resource passes a given test.
|
237
|
+
#
|
238
|
+
# **IMPORTANT** Tests are executed in the current controller context.
|
239
|
+
#
|
240
|
+
# @param [Symbol] _test test identifier.
|
241
|
+
# @param [Symbol|String] :on optional key or expression used to retrieve
|
242
|
+
# from the resource the value to be passed to the test instead of the resource.
|
243
|
+
#
|
244
|
+
def certifies(_test, _options={})
|
245
|
+
test = @tests[_test]
|
246
|
+
raise SetupError.new "Invalid test identifier '#{_test}'" if test.nil?
|
247
|
+
if test.arity == 1
|
248
|
+
user_value = self.class.load_value_for(@res, _options[:on])
|
249
|
+
@ctx.instance_exec(user_value, &test)
|
250
|
+
else @ctx.instance_eval &test end
|
251
|
+
end
|
252
|
+
alias :checks :certifies
|
253
|
+
|
254
|
+
## Tests whether a given expression evaluated in the resource context returns true.
|
255
|
+
#
|
256
|
+
# **IMPORTANT** if no resource is provided the current controller instance is used instead.
|
257
|
+
#
|
258
|
+
# @param [Symbol|String] _what if symbol, then send is used to call a context's
|
259
|
+
# function with that name, if a string, then instance_eval is used to evaluate it.
|
260
|
+
def holds(_what)
|
261
|
+
_what.is_a? Symbol ? @res.send(_what) : @res.instance_eval(_what.to_s)
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
## Definition module
|
267
|
+
#
|
268
|
+
# This module is used to generate a canned definition that can later
|
269
|
+
# be refered when calling "canned_setup".
|
270
|
+
#
|
271
|
+
# TODO: Usage
|
272
|
+
#
|
273
|
+
module Definition
|
274
|
+
|
275
|
+
def self.included(klass)
|
276
|
+
klass.extend ClassMethods
|
277
|
+
end
|
278
|
+
|
279
|
+
module ClassMethods
|
280
|
+
|
281
|
+
@@tests = {}
|
282
|
+
@@profiles = {}
|
283
|
+
|
284
|
+
## Defines a new test that can be used in "certifies" instructions
|
285
|
+
#
|
286
|
+
# **IMPORTANT** Tests are executed in the controller's context and
|
287
|
+
# passed the tested resource as parameter (only if arity == 1)
|
288
|
+
#
|
289
|
+
# @param [Symbol] _name test identifier
|
290
|
+
# @param [Block] _block test block
|
291
|
+
#
|
292
|
+
def test(_name, &_block)
|
293
|
+
raise SetupError.new "Invalid test arity for '#{_name}'" if _block.arity > 1
|
294
|
+
raise SetupError.new "Duplicated test identifier" if @@tests.has_key? _name
|
295
|
+
@@tests[_name] = _block
|
296
|
+
end
|
297
|
+
|
298
|
+
## Creates a new profile and evaluates the given block using the profile context.
|
299
|
+
#
|
300
|
+
# @param [String|Symbol] _name Profile name.
|
301
|
+
# @param [String|Symbol] :inherits Name of profile to inherit rules from.
|
302
|
+
# @param [Symbol] :matcher Default matcher for matches tests
|
303
|
+
# @param [Symbol] :resource Default resource for upon expressions
|
304
|
+
#
|
305
|
+
def profile(_name, _options={}, &_block)
|
306
|
+
profile = @@profiles[_name.to_s] = Profile.new(
|
307
|
+
@@profiles[_options.fetch(:inherits, nil).to_s],
|
308
|
+
_options.fetch(:matcher, :equals),
|
309
|
+
_options.fetch(:resource, nil)
|
310
|
+
)
|
311
|
+
profile.instance_eval &_block
|
312
|
+
end
|
313
|
+
|
314
|
+
# @api callback
|
315
|
+
def can?(_ctx, _profile, _action)
|
316
|
+
profile = @@profiles[_profile.to_s]
|
317
|
+
return if profile.nil?
|
318
|
+
profile.rules[_action].any? do |rule|
|
319
|
+
next true if rule.nil?
|
320
|
+
rule_ctx = RuleContext.new _ctx, @@tests, profile.def_matcher, profile.def_resource
|
321
|
+
rule_ctx.instance_eval(&rule)
|
322
|
+
rule_ctx.passed?
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
data/lib/platanus/version.rb
CHANGED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'platanus/canned2'
|
3
|
+
|
4
|
+
describe Platanus::Canned2 do
|
5
|
+
|
6
|
+
class DummyUsr
|
7
|
+
|
8
|
+
attr_reader :char1
|
9
|
+
attr_reader :char2
|
10
|
+
|
11
|
+
def initialize(_char1=nil, _char2=nil)
|
12
|
+
@char1 = _char1
|
13
|
+
@char2 = _char2
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class DummyCtx
|
18
|
+
|
19
|
+
attr_reader :params
|
20
|
+
attr_reader :current_user
|
21
|
+
|
22
|
+
def initialize(_user, _params={})
|
23
|
+
@current_user = _user
|
24
|
+
@params = _params
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Roles
|
29
|
+
include Platanus::Canned2::Definition
|
30
|
+
|
31
|
+
test :test1 do
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
profile :user, matcher: :equals_int do
|
36
|
+
|
37
|
+
# Simple allows
|
38
|
+
allow 'rute1#action1'
|
39
|
+
allow 'rute1#action2', upon(:current_user) { matches(:char1) }
|
40
|
+
allow 'rute1#action3', upon { match(:char1, on: "current_user.char1") }
|
41
|
+
allow 'rute1#action4', upon(:current_user) { matches(:param2, on: "char2") and checks(:test1) }
|
42
|
+
|
43
|
+
# Complex routes
|
44
|
+
allow 'rute1#action5' do
|
45
|
+
upon(:current_user) { matches(:char1) }
|
46
|
+
upon(:current_user) { matches(:param2, value: 55) or checks(:test1) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
let(:good_ctx) { DummyCtx.new(DummyUsr.new(10, "200"), char1: '10', param2: '200') }
|
52
|
+
let(:bad_ctx) { DummyCtx.new(DummyUsr.new(10, 30), char1: '10', param2: '200') }
|
53
|
+
|
54
|
+
describe "._run" do
|
55
|
+
context 'when using single context rules' do
|
56
|
+
|
57
|
+
it "does authorize on empty rute" do
|
58
|
+
Roles.can?(good_ctx, :user, 'rute1#action1').should be_true
|
59
|
+
end
|
60
|
+
it "does authorize on rute with context and match" do
|
61
|
+
Roles.can?(good_ctx, :user, 'rute1#action2').should be_true
|
62
|
+
end
|
63
|
+
it "does authorize on rute without context and match" do
|
64
|
+
Roles.can?(good_ctx, :user, 'rute1#action3').should be_true
|
65
|
+
end
|
66
|
+
it "does authorize on rute with context, match and test" do
|
67
|
+
Roles.can?(good_ctx, :user, 'rute1#action4').should be_true
|
68
|
+
end
|
69
|
+
it "does not authorize on rute with context, match and test with bad credentials" do
|
70
|
+
Roles.can?(bad_ctx, :user, 'rute1#action4').should be_false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when using multiple context rules' do
|
75
|
+
it "does authorize on rute with context, match and test" do
|
76
|
+
Roles.can?(good_ctx, :user, 'rute1#action5').should be_true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "canned_setup" do
|
82
|
+
end
|
83
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: platanus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.26
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-26 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Platan.us utility gem
|
15
15
|
email:
|
@@ -28,6 +28,7 @@ files:
|
|
28
28
|
- lib/platanus/activable.rb
|
29
29
|
- lib/platanus/api_base.rb
|
30
30
|
- lib/platanus/canned.rb
|
31
|
+
- lib/platanus/canned2.rb
|
31
32
|
- lib/platanus/enum.rb
|
32
33
|
- lib/platanus/gcontroller.rb
|
33
34
|
- lib/platanus/http_helpers.rb
|
@@ -42,7 +43,7 @@ files:
|
|
42
43
|
- lib/platanus/validators/rut.rb
|
43
44
|
- lib/platanus/version.rb
|
44
45
|
- platanus.gemspec
|
45
|
-
- spec/
|
46
|
+
- spec/canned2_spec.rb
|
46
47
|
- spec/spec_helper.rb
|
47
48
|
homepage: http://www.platan.us
|
48
49
|
licenses: []
|
@@ -70,6 +71,6 @@ signing_key:
|
|
70
71
|
specification_version: 3
|
71
72
|
summary: This gem contains various ruby classes used by Platanus in our rails proyects
|
72
73
|
test_files:
|
73
|
-
- spec/
|
74
|
+
- spec/canned2_spec.rb
|
74
75
|
- spec/spec_helper.rb
|
75
76
|
has_rdoc:
|