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.
@@ -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,8 @@
1
+ module Canned
2
+ module Context
3
+ class Multi < Base
4
+ include Matchers::Where
5
+ include Matchers::Plus
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module Canned
2
+ module Context
3
+ class Resource < Base
4
+ include Matchers::Where
5
+ include Matchers::Has
6
+ include Matchers::Is
7
+ include Matchers::That
8
+ include Matchers::Relation
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Canned
2
+ module Context
3
+ class Value < Resource
4
+ include Matchers::Equality
5
+ end
6
+ end
7
+ 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,6 @@
1
+ module Canned
2
+ class Error < StandardError; end
3
+ class SetupError < Error; end
4
+ class AuthError < Error; end
5
+ class ForbiddenError < AuthError; end
6
+ 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