canned 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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