canned 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +39 -0
- data/Rakefile +23 -0
- data/lib/canned.rb +9 -0
- data/lib/canned/context/actor.rb +31 -0
- data/lib/canned/context/base.rb +40 -0
- data/lib/canned/context/default.rb +10 -0
- data/lib/canned/context/matchers/asks_for.rb +19 -0
- data/lib/canned/context/matchers/asks_with.rb +36 -0
- data/lib/canned/context/matchers/equality.rb +48 -0
- data/lib/canned/context/matchers/has.rb +23 -0
- data/lib/canned/context/matchers/helpers.rb +13 -0
- data/lib/canned/context/matchers/is.rb +23 -0
- data/lib/canned/context/matchers/load.rb +26 -0
- data/lib/canned/context/matchers/plus.rb +19 -0
- data/lib/canned/context/matchers/relation.rb +52 -0
- data/lib/canned/context/matchers/that.rb +23 -0
- data/lib/canned/context/matchers/the.rb +24 -0
- data/lib/canned/context/matchers/where.rb +36 -0
- data/lib/canned/context/multi.rb +8 -0
- data/lib/canned/context/resource.rb +11 -0
- data/lib/canned/context/value.rb +7 -0
- data/lib/canned/controller_ext.rb +216 -0
- data/lib/canned/definition.rb +79 -0
- data/lib/canned/errors.rb +6 -0
- data/lib/canned/profile.rb +62 -0
- data/lib/canned/profile_dsl.rb +130 -0
- data/lib/canned/stack.rb +63 -0
- data/lib/canned/version.rb +3 -0
- data/spec/canned/canned_spec.rb +116 -0
- data/spec/canned/controller_ext_spec.rb +95 -0
- data/spec/spec_helper.rb +35 -0
- metadata +113 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Canned
|
2
|
+
module Context
|
3
|
+
module Matchers
|
4
|
+
module That
|
5
|
+
|
6
|
+
def that(&_block)
|
7
|
+
if _block
|
8
|
+
instance_eval &_block
|
9
|
+
else self end
|
10
|
+
end
|
11
|
+
|
12
|
+
def that_all(&_block)
|
13
|
+
# TODO
|
14
|
+
end
|
15
|
+
|
16
|
+
def that_any(&_block)
|
17
|
+
# TODO
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Canned
|
2
|
+
module Context
|
3
|
+
module Matchers
|
4
|
+
module The
|
5
|
+
|
6
|
+
## Loads an actor and returns an actor context.
|
7
|
+
#
|
8
|
+
# @param [String|Symbol] _name Actor name
|
9
|
+
# @param [Hash] _options Various options:
|
10
|
+
# * as: If given, the actor will use **as** as alias for **where** blocks instead of the name.
|
11
|
+
# @param [Block] _block If given, then the block will be evaluated in the actor's context and the result
|
12
|
+
# of that returned by this function.
|
13
|
+
#
|
14
|
+
def the(_name, _options={}, &_block)
|
15
|
+
_chain_context(Canned::Context::Actor, _block) do |stack|
|
16
|
+
actor = @ctx.actors[_name]
|
17
|
+
break false if actor.nil?
|
18
|
+
stack.push(:actor, _options.fetch(:as, _name), actor)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Canned
|
2
|
+
module Context
|
3
|
+
module Matchers
|
4
|
+
module Where
|
5
|
+
|
6
|
+
class WhereCtx
|
7
|
+
def initialize(_stack)
|
8
|
+
@stack = _stack
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(_method, *_args, &_block)
|
12
|
+
if _args.count == 0 and _block.nil?
|
13
|
+
begin
|
14
|
+
return @stack.resolve(_method)
|
15
|
+
rescue Canned::InmmutableStack::NotFound; end
|
16
|
+
end
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
## Executes a given block using current resources.
|
22
|
+
#
|
23
|
+
# Examples:
|
24
|
+
# upon { the(:actor) { loads(:resource).where { actor.res_id == resource.id } } }
|
25
|
+
#
|
26
|
+
# @param [Block] _block Block to be evaluated.
|
27
|
+
# @returns [Boolean] True if conditions are met.
|
28
|
+
#
|
29
|
+
def where(&_block)
|
30
|
+
return false unless indeed?
|
31
|
+
WhereCtx.new(@stack).instance_eval(&_block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require "canned/definition"
|
2
|
+
|
3
|
+
module Canned
|
4
|
+
|
5
|
+
## Action Controller extension
|
6
|
+
#
|
7
|
+
# Include this in the the base application controller and use the acts_as_restricted method to seal it.
|
8
|
+
#
|
9
|
+
# ApplicationController << ActionController:Base
|
10
|
+
# include Canned:ControllerExt
|
11
|
+
#
|
12
|
+
# # Call canned setup method passing the desired profile definition object
|
13
|
+
# acts_as_restricted Profiles do
|
14
|
+
#
|
15
|
+
# # Put authentication code here...
|
16
|
+
#
|
17
|
+
# # Return profiles you wish to validate
|
18
|
+
# [:profile_1, :profile_2]
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
module ControllerExt
|
24
|
+
|
25
|
+
def self.included(klass)
|
26
|
+
class << klass
|
27
|
+
attr_accessor :_cn_actors
|
28
|
+
attr_accessor :_cn_excluded
|
29
|
+
attr_accessor :_cn_resources
|
30
|
+
end
|
31
|
+
|
32
|
+
# actors are shared between subclasses
|
33
|
+
klass.cattr_accessor :_cn_actors
|
34
|
+
klass._cn_actors = ActiveSupport::HashWithIndifferentAccess.new
|
35
|
+
|
36
|
+
klass.extend ClassMethods
|
37
|
+
end
|
38
|
+
|
39
|
+
## Performs access authorization for current action
|
40
|
+
#
|
41
|
+
# @param [Definition] _definition Profile definition
|
42
|
+
# @param [Array<String>] _profiles Profiles to validate
|
43
|
+
# @returns [Boolean] True if action access is authorized
|
44
|
+
#
|
45
|
+
def perform_access_authorization(_definition, _profiles)
|
46
|
+
# preload resources, retrieve resource proxy
|
47
|
+
proxy = perform_resource_loading
|
48
|
+
|
49
|
+
# run profile validation
|
50
|
+
result = false
|
51
|
+
_profiles.each do |profile|
|
52
|
+
case _definition.validate proxy, profile, [controller_name, "#{controller_name}##{action_name}"]
|
53
|
+
when :forbidden then return false
|
54
|
+
when :allowed then result = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
return result
|
58
|
+
end
|
59
|
+
|
60
|
+
## Performs resource loading for current action
|
61
|
+
#
|
62
|
+
# @returns [ControllerProxy] used for resource loading
|
63
|
+
#
|
64
|
+
def perform_resource_loading
|
65
|
+
proxy = ControllerProxy.new self
|
66
|
+
proxy.preload_resources_for action_name
|
67
|
+
return proxy
|
68
|
+
end
|
69
|
+
|
70
|
+
## Returns true if the current action is protected.
|
71
|
+
#
|
72
|
+
def is_restricted?
|
73
|
+
return true if self.class._cn_excluded.nil?
|
74
|
+
return false if self.class._cn_excluded == :all
|
75
|
+
return !(self.class._cn_excluded.include? action_name.to_sym)
|
76
|
+
end
|
77
|
+
|
78
|
+
module ClassMethods
|
79
|
+
|
80
|
+
## Setups the controller user profile definitions and profile provider block (or proc)
|
81
|
+
#
|
82
|
+
# The passed method or block must return a list of profiles to be validated
|
83
|
+
# by the definition.
|
84
|
+
#
|
85
|
+
# TODO: default definition (canned config)
|
86
|
+
#
|
87
|
+
# @param [Definition] _definition Profile definition
|
88
|
+
# @param [Symbol] _method Profile provider method name
|
89
|
+
# @param [Block] _block Profile provider block
|
90
|
+
#
|
91
|
+
def acts_as_restricted(_definition, _method=nil, &_block)
|
92
|
+
self.before_filter do
|
93
|
+
if is_restricted?
|
94
|
+
profiles = Array(if _method.nil? then instance_eval(&_block) else send(_method) end)
|
95
|
+
raise Canned::AuthError.new 'No profiles avaliable' if profiles.empty?
|
96
|
+
raise Canned::AuthError unless perform_access_authorization(_definition, profiles)
|
97
|
+
else perform_resource_loading end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
## Removes protection for all controller actions.
|
102
|
+
def unrestricted_all
|
103
|
+
self._cn_excluded = :all
|
104
|
+
end
|
105
|
+
|
106
|
+
## Removes protection for the especified controller actions.
|
107
|
+
#
|
108
|
+
# @param [splat] _excluded List of actions to be excluded.
|
109
|
+
#
|
110
|
+
def unrestricted(*_excluded)
|
111
|
+
self._cn_excluded ||= []
|
112
|
+
self._cn_excluded.push(*(_excluded.collect &:to_sym))
|
113
|
+
end
|
114
|
+
|
115
|
+
## Registers a canned actor
|
116
|
+
#
|
117
|
+
# @param [String] _name Actor's name and generator method name if no block is given.
|
118
|
+
# @param [Hash] _options Options:
|
119
|
+
# * as: If given, this si used as actor's name and _name is only used for generator retrieval.
|
120
|
+
# @param [Block] _block generator block, used instead of generator method if given.
|
121
|
+
#
|
122
|
+
def register_actor(_name, _options={}, &_block)
|
123
|
+
self._cn_actors[_options.fetch(:as, _name)] = _block || _name
|
124
|
+
end
|
125
|
+
|
126
|
+
## Registers a canned resource
|
127
|
+
#
|
128
|
+
# @param [String] _name Resource name
|
129
|
+
# @param [String] _options Options:
|
130
|
+
# * using: Parameter used as key if not block is given.
|
131
|
+
# * only: If set, will only load the resource for the given actions.
|
132
|
+
# * except: If set, will not load the resource for any of the given actions.
|
133
|
+
# * from: TODO load_resource :raffle, from: :site
|
134
|
+
# * as: TODO: load_resource :raffle, from: :site, as: :draws
|
135
|
+
# @param [Block] _block generator block, will be called to generate the resource if needed.
|
136
|
+
#
|
137
|
+
def register_resource(_name, _options={}, &_block)
|
138
|
+
self._cn_resources ||= []
|
139
|
+
self._cn_resources << {
|
140
|
+
name: _name,
|
141
|
+
only: unless _options[:only].nil? then Array(_options[:only]) else nil end,
|
142
|
+
except: Array(_options[:except]),
|
143
|
+
loader: _block || Proc.new do
|
144
|
+
key = _options.fetch(:using, :id)
|
145
|
+
if params.has_key? key then eval(_name.to_s.camelize).find params[key]
|
146
|
+
else nil end
|
147
|
+
end
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def register_default_resources
|
152
|
+
# TODO: Load resources using convention and controller names.
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
## ActionController - TestContext adapter
|
159
|
+
class ControllerProxy
|
160
|
+
|
161
|
+
attr_reader :resources
|
162
|
+
attr_reader :actors
|
163
|
+
|
164
|
+
def initialize(_controller)
|
165
|
+
@controller = _controller
|
166
|
+
@resources = ActiveSupport::HashWithIndifferentAccess.new
|
167
|
+
# actors are provided throug a dynamic loader.
|
168
|
+
@actors = ActorLoader.new _controller, _controller.class._cn_actors
|
169
|
+
end
|
170
|
+
|
171
|
+
## Proxies messages to wrapped controller.
|
172
|
+
def method_missing(_method, *_args, &_block)
|
173
|
+
@controller.send(_method, *_args, &_block)
|
174
|
+
end
|
175
|
+
|
176
|
+
## Loads resources required by _action
|
177
|
+
def preload_resources_for(_action)
|
178
|
+
_action = _action.to_sym
|
179
|
+
Array(@controller.class._cn_resources).each do |res|
|
180
|
+
next unless res[:only].nil? or res[:only].include? _action
|
181
|
+
next unless res[:except].nil? or !res[:except].include? _action
|
182
|
+
|
183
|
+
@resources[res[:name]] = resource = @controller.instance_eval &res[:loader]
|
184
|
+
@controller.instance_variable_set "@#{res[:name]}", resource
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
## Allows actors to be served on demand, provides a hash–like interface
|
191
|
+
# to TestContext through ControllerProxy.actors method.
|
192
|
+
class ActorLoader
|
193
|
+
|
194
|
+
def initialize(_controller, _loaders)
|
195
|
+
@controller = _controller
|
196
|
+
@loaders = _loaders
|
197
|
+
@actor_cache = {}
|
198
|
+
end
|
199
|
+
|
200
|
+
def [](_key)
|
201
|
+
_key = _key.to_sym
|
202
|
+
return @actor_cache[_key] if @actor_cache.has_key? _key
|
203
|
+
|
204
|
+
loader = @loaders[_key]
|
205
|
+
raise Canned::SetupError.new "Invalid actor loader value" if loader.nil?
|
206
|
+
actor = if loader.is_a? String then @controller.send(loader) else @controller.instance_eval(&loader) end
|
207
|
+
@actor_cache[_key] = actor
|
208
|
+
end
|
209
|
+
|
210
|
+
def has_key?(_key)
|
211
|
+
@loaders.has_key? _key.to_sym
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "canned/errors"
|
2
|
+
require "canned/stack"
|
3
|
+
require "canned/profile"
|
4
|
+
require "canned/profile_dsl"
|
5
|
+
|
6
|
+
require "canned/context/base"
|
7
|
+
require "canned/context/default"
|
8
|
+
require "canned/context/actor"
|
9
|
+
require "canned/context/resource"
|
10
|
+
require "canned/context/value"
|
11
|
+
require "canned/context/multi"
|
12
|
+
|
13
|
+
Dir[File.dirname(__FILE__) + '/context/*.rb'].each { |file| require file }
|
14
|
+
|
15
|
+
|
16
|
+
module Canned
|
17
|
+
|
18
|
+
## Definition module
|
19
|
+
#
|
20
|
+
# This module is used to generate a canned definition that can later
|
21
|
+
# be refered when calling "canned_setup".
|
22
|
+
#
|
23
|
+
# TODO: Usage
|
24
|
+
#
|
25
|
+
module Definition
|
26
|
+
|
27
|
+
def self.included(klass)
|
28
|
+
klass.class_eval("
|
29
|
+
@@tests = {}
|
30
|
+
@@profiles = {}
|
31
|
+
|
32
|
+
def self.tests; @@tests end
|
33
|
+
def self.profiles; @@profiles end
|
34
|
+
", __FILE__, __LINE__ + 1)
|
35
|
+
klass.extend ClassMethods
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
|
40
|
+
## Defines a new test that can be used in "certifies" instructions
|
41
|
+
#
|
42
|
+
# **IMPORTANT** Tests are executed in the same context as upon blocks,
|
43
|
+
#
|
44
|
+
# @param [Symbol] _name test identifier
|
45
|
+
# @param [Block] _block test block, arity must be 0
|
46
|
+
#
|
47
|
+
def test(_name, &_block)
|
48
|
+
raise SetupError.new "Duplicated test identifier" if tests.has_key? _name
|
49
|
+
end
|
50
|
+
|
51
|
+
## Creates a new profile and evaluates the given block using the profile context.
|
52
|
+
#
|
53
|
+
# @param [String|Symbol] _name Profile name.
|
54
|
+
# @param [Hash] _options Various options: none for now
|
55
|
+
#
|
56
|
+
def profile(_name, _options={}, &_block)
|
57
|
+
_name = _name.to_sym
|
58
|
+
raise Canned::SetupError.new "Duplicated profile identifier '#{_name}'" if profiles.has_key? _name
|
59
|
+
|
60
|
+
profile = Canned::Profile.new
|
61
|
+
ProfileDsl.new(profile, profiles).instance_eval &_block
|
62
|
+
profiles[_name] = profile
|
63
|
+
end
|
64
|
+
|
65
|
+
## Returns true if **_action** is avaliable for **_profile** under the given **_ctx**.
|
66
|
+
#
|
67
|
+
# @param [Canned2::TestContext] _ctx The test context to be used
|
68
|
+
# @param [string] _acting_as The name of profile to be tested
|
69
|
+
# @param [String|Array<String>] _actions The action or actions to test
|
70
|
+
#
|
71
|
+
def validate(_ctx, _acting_as, _action)
|
72
|
+
profile = profiles[_acting_as.to_sym]
|
73
|
+
raise Canned::SetupError.new "Profile not found '#{_acting_as}'" if profile.nil?
|
74
|
+
_ctx = Canned::Context::Default.new(_ctx, tests, InmmutableStack.new)
|
75
|
+
profile.validate _ctx, Array(_action)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Canned
|
2
|
+
|
3
|
+
## Holds a profile definition and provides the **validate** method
|
4
|
+
#
|
5
|
+
# This class instances are populated using the **ProfileDsl**.
|
6
|
+
#
|
7
|
+
# profile :hola do
|
8
|
+
# context { the(:user) }
|
9
|
+
# context { a(:raffle) }
|
10
|
+
#
|
11
|
+
# allow 'index', upon(:admin) { loads(:) where { } } }
|
12
|
+
# allow 'index', upon { the(:user_id) { is } and a(:raffle).is }
|
13
|
+
# allow 'show', upon { is :is_allowed? and has() and a(:apron).has(:id).same_as(own: :id) asks_for(:) and owns(:raffle) } }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
class Profile
|
17
|
+
|
18
|
+
attr_accessor :context
|
19
|
+
attr_accessor :rules
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@context = nil
|
23
|
+
@rules = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate(_base, _actions)
|
27
|
+
|
28
|
+
# TODO: optimize, do not process allow rules if already allowed.
|
29
|
+
|
30
|
+
# run the context block if given
|
31
|
+
# TODO: check base type when a context is used?
|
32
|
+
_base = _base.instance_eval &@context if @context
|
33
|
+
|
34
|
+
@rules.each do |rule|
|
35
|
+
case rule[:type]
|
36
|
+
when :allow
|
37
|
+
if rule[:action].nil? or _actions.include? rule[:action]
|
38
|
+
return :allowed if rule[:proc].nil? or _base.instance_eval(&rule[:proc])
|
39
|
+
end
|
40
|
+
when :forbid
|
41
|
+
if rule[:action].nil? or _actions.include? rule[:action]
|
42
|
+
return :forbidden if rule[:proc].nil? or _base.instance_eval(&rule[:proc])
|
43
|
+
end
|
44
|
+
when :continue
|
45
|
+
# continue block's interrupt flow if false
|
46
|
+
return :break unless _base.instance_eval(&rule[:proc])
|
47
|
+
when :expand
|
48
|
+
# when evaluating an cross profile call, any special condition will cause to break.
|
49
|
+
result = rule[:profile].validate(_base, _actions)
|
50
|
+
return result if result != :default
|
51
|
+
when :scope
|
52
|
+
# when evaluating a child block, only break if a matching allow or forbid is found.
|
53
|
+
result = rule[:profile].validate(_base, _actions)
|
54
|
+
return result if result != :default and result != :break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# No rule matched, return not allowed.
|
59
|
+
return :default
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|