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 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