canned 0.1.4
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 +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
|