platanus 0.0.25 → 0.0.26
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|