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 CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
3
  gem 'rspec'
4
+ gem 'rspec-expectations'
5
+
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Platanus
2
- VERSION = "0.0.25" # 0.1 will come with tests!
2
+ VERSION = "0.0.26" # 0.1 will come with tests!
3
3
  end
@@ -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.25
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-23 00:00:00.000000000 Z
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/canned_spec.rb
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/canned_spec.rb
74
+ - spec/canned2_spec.rb
74
75
  - spec/spec_helper.rb
75
76
  has_rdoc:
data/spec/canned_spec.rb DELETED
@@ -1,5 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Platanus::Canned do
4
- pending 'Write This'
5
- end