cant 0.2.0
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/.gitignore +7 -0
- data/.rspec +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +69 -0
- data/Guardfile +8 -0
- data/LICENSE.mit +22 -0
- data/README.markdown +204 -0
- data/Rakefile +6 -0
- data/cant.gemspec +52 -0
- data/lib/cant/embeddable.rb +39 -0
- data/lib/cant/engine.rb +175 -0
- data/lib/cant/version.rb +3 -0
- data/lib/cant.rb +2 -0
- data/spec/cant/embeddable_spec.rb +43 -0
- data/spec/cant/engine_spec.rb +148 -0
- data/spec/integration_spec.rb +17 -0
- data/spec/models/user.rb +8 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/stunts/stunt.rb +11 -0
- metadata +113 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
cant (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ParseTree (3.0.6)
|
10
|
+
RubyInline (>= 3.7.0)
|
11
|
+
sexp_processor (>= 3.0.0)
|
12
|
+
RubyInline (3.8.6)
|
13
|
+
ZenTest (~> 4.3)
|
14
|
+
ZenTest (4.4.2)
|
15
|
+
configuration (1.2.0)
|
16
|
+
diff-lcs (1.1.2)
|
17
|
+
file-tail (1.0.5)
|
18
|
+
spruz (>= 0.1.0)
|
19
|
+
growl (1.0.3)
|
20
|
+
guard (0.2.2)
|
21
|
+
open_gem (~> 1.4.2)
|
22
|
+
thor (~> 0.14.3)
|
23
|
+
guard-rspec (0.1.9)
|
24
|
+
guard (>= 0.2.2)
|
25
|
+
launchy (0.3.7)
|
26
|
+
configuration (>= 0.0.5)
|
27
|
+
rake (>= 0.8.1)
|
28
|
+
open_gem (1.4.2)
|
29
|
+
launchy (~> 0.3.5)
|
30
|
+
predicated (0.2.2)
|
31
|
+
rake (0.8.7)
|
32
|
+
rspec (2.3.0)
|
33
|
+
rspec-core (~> 2.3.0)
|
34
|
+
rspec-expectations (~> 2.3.0)
|
35
|
+
rspec-mocks (~> 2.3.0)
|
36
|
+
rspec-core (2.3.0)
|
37
|
+
rspec-expectations (2.3.0)
|
38
|
+
diff-lcs (~> 1.1.2)
|
39
|
+
rspec-mocks (2.3.0)
|
40
|
+
ruby2ruby (1.2.5)
|
41
|
+
ruby_parser (~> 2.0)
|
42
|
+
sexp_processor (~> 3.0)
|
43
|
+
ruby_parser (2.0.5)
|
44
|
+
sexp_processor (~> 3.0)
|
45
|
+
sexp_processor (3.0.5)
|
46
|
+
sourcify (0.4.0)
|
47
|
+
ruby2ruby (>= 1.2.5)
|
48
|
+
sexp_processor (>= 3.0.5)
|
49
|
+
spruz (0.2.2)
|
50
|
+
thor (0.14.6)
|
51
|
+
wrong (0.5.0)
|
52
|
+
ParseTree (~> 3.0)
|
53
|
+
diff-lcs (~> 1.1.2)
|
54
|
+
file-tail (~> 1.0)
|
55
|
+
predicated (>= 0.2.2)
|
56
|
+
ruby2ruby (~> 1.2)
|
57
|
+
ruby_parser (~> 2.0.4)
|
58
|
+
sexp_processor (~> 3.0)
|
59
|
+
sourcify (>= 0.3.0)
|
60
|
+
|
61
|
+
PLATFORMS
|
62
|
+
ruby
|
63
|
+
|
64
|
+
DEPENDENCIES
|
65
|
+
cant!
|
66
|
+
growl
|
67
|
+
guard-rspec
|
68
|
+
rspec
|
69
|
+
wrong
|
data/Guardfile
ADDED
data/LICENSE.mit
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2010 Thierry Henrio
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
Intent
|
2
|
+
======
|
3
|
+
|
4
|
+
Lightweight authorization library, that let you choose where and how you write your rules
|
5
|
+
|
6
|
+
Cant is agnostic : you can use it in any existing or future framework, provided names don't clash
|
7
|
+
|
8
|
+
Cant is small and even simple : it is 120 lines of code, has no dependencies but for testing
|
9
|
+
|
10
|
+
Cant can be configured at class level, and support basic configuration inheritance
|
11
|
+
|
12
|
+
|
13
|
+
What does it look like ?
|
14
|
+
========================
|
15
|
+
|
16
|
+
Examples
|
17
|
+
--------
|
18
|
+
|
19
|
+
* in an existing model
|
20
|
+
|
21
|
+
class User
|
22
|
+
include Cant::Embeddable
|
23
|
+
|
24
|
+
cant do |action, code|
|
25
|
+
(action == :post and Code === code) if drunken?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
deny {bob.cant? :post, kata}
|
30
|
+
|
31
|
+
* in a separate model
|
32
|
+
|
33
|
+
class Permission
|
34
|
+
include Cant::Embeddable
|
35
|
+
|
36
|
+
cant do |user, action, resource|
|
37
|
+
(action == :post and Code === resource) if user.drunken?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
assert {permission.cant? jackie, :post, kata}
|
42
|
+
|
43
|
+
* in a rails controller
|
44
|
+
|
45
|
+
class ApplicationController < ActionController::Base
|
46
|
+
include Cant::Embeddable
|
47
|
+
alias_method :authorize_user!, :die_if_cant!
|
48
|
+
helper_method :cant?
|
49
|
+
|
50
|
+
rescue_from Cant::AccessDenied do |error|
|
51
|
+
flash[:error] = error.message
|
52
|
+
redirect_to request.referer
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class KatasController < ApplicationController
|
57
|
+
before_filter :authenticate_user!, :authorize_user!
|
58
|
+
|
59
|
+
cant do |request|
|
60
|
+
current_user.drunken? if request.method == 'POST'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
* in a rack middleware ... I have not experimented yet ...
|
65
|
+
|
66
|
+
breaking authorization into small independent pieces is valuable, and you do not need a library for that, though using Cant::Editable and Cant::Questionable mixins can help you do that
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
Concepts
|
71
|
+
========
|
72
|
+
|
73
|
+
Cant is simply put a reduce (or [fold](http://learnyousomeerlang.com/higher-order-functions)) on a list of Rules.
|
74
|
+
|
75
|
+
* __Rule__
|
76
|
+
|
77
|
+
a tuple of functions {__predicate__, __die__}.
|
78
|
+
|
79
|
+
* __rules__
|
80
|
+
|
81
|
+
a list of __Rule__s
|
82
|
+
|
83
|
+
* __fold__
|
84
|
+
|
85
|
+
function defining how __rules__ are traversed, and how each predicate is evaluated, and what is the result
|
86
|
+
|
87
|
+
Cant provide two fold functions, returning both first __Rule__ that predicates, or nil
|
88
|
+
|
89
|
+
* __predicate__
|
90
|
+
|
91
|
+
default rationale is : true means cant
|
92
|
+
|
93
|
+
* __die__
|
94
|
+
|
95
|
+
a function, that can raise or return any result suitable to your need
|
96
|
+
|
97
|
+
* __cant?__
|
98
|
+
|
99
|
+
calls fold function to operate on __rules__
|
100
|
+
|
101
|
+
* __die\_if\_cant!__
|
102
|
+
|
103
|
+
calls fold function to operate on __rules__, and calls die function on result
|
104
|
+
|
105
|
+
How do I define rules ?
|
106
|
+
=======================
|
107
|
+
|
108
|
+
First, choose where you want to define your list, and what information a Rule will need
|
109
|
+
|
110
|
+
The point of cant is to define a lists of similar rules together, so that they will require similar informations
|
111
|
+
|
112
|
+
A list of rules can be embedded in an existing class using Cant::Embeddable mixin
|
113
|
+
|
114
|
+
* define rule and functions at class level
|
115
|
+
* evaluate predicates at instance level
|
116
|
+
|
117
|
+
Then there is one list of rules for any instance of this class, and instance evaluation leads to terser rules
|
118
|
+
|
119
|
+
Note that a list of rules can be shared by many inquirers, either explicitly or by using class instance variable inheritance feature
|
120
|
+
|
121
|
+
Default Values
|
122
|
+
--------------
|
123
|
+
|
124
|
+
__Cant__ module has "reasonable" default values for __fold__, __die__, __rules__
|
125
|
+
|
126
|
+
The Cant::Editable module gather methods to configure a Cant engine (fold, die, rules), and defaults to Cant module values
|
127
|
+
|
128
|
+
Inheritance
|
129
|
+
-----------
|
130
|
+
|
131
|
+
Cant support _basic_ inheritance functionality for configuration with the Cant::Embeddable module ...
|
132
|
+
Ouch what that means ?
|
133
|
+
|
134
|
+
Given Admin inherits from User
|
135
|
+
And User.die {"I'm not dead!"}
|
136
|
+
Then assert {Admin.die.call == "I'm not dead!"} is true
|
137
|
+
|
138
|
+
Well, have a look at read documentation and source code embeddable.rb if you are having trouble with this functionality
|
139
|
+
|
140
|
+
What is the arity of a __predicate__ function ?
|
141
|
+
-----------------------------------------------
|
142
|
+
|
143
|
+
You are free to pick one that suit your needs
|
144
|
+
|
145
|
+
There are a couple of things to drive your choice :
|
146
|
+
|
147
|
+
* params of __cant?__ are passed to __predicate__
|
148
|
+
|
149
|
+
* params of __die\_if\_cant!__ are passed to __predicate__ and __die__
|
150
|
+
|
151
|
+
* number and order of params of in a rule list should be the same
|
152
|
+
|
153
|
+
* the context of predicate evaluation (that is defined in fold function)
|
154
|
+
the receiver methods will be available in function
|
155
|
+
|
156
|
+
* a container can be a handy parameter (Hash)
|
157
|
+
_env_, _params_
|
158
|
+
|
159
|
+
* a block is a proc, and can use default values for params
|
160
|
+
|
161
|
+
So pick your own semantic, or grow an existing one
|
162
|
+
|
163
|
+
How do I verify authorization ?
|
164
|
+
===============================
|
165
|
+
|
166
|
+
Cant very meaning is : you can unless you cant, and you define what you cant
|
167
|
+
|
168
|
+
Defining _not_ or _negative_ ability require some thinking, and I believe we can do it :)
|
169
|
+
|
170
|
+
Use __cant?__ method to check
|
171
|
+
|
172
|
+
Use __die\_if\_cant!__ method to check and run __die__ code
|
173
|
+
|
174
|
+
When you check, provide the list of parameters you specified in your list of rules
|
175
|
+
|
176
|
+
Inspired from
|
177
|
+
=============
|
178
|
+
|
179
|
+
* [cancan](https://github.com/ryanb/cancan), as I started with it and saw that it did not functioned in presence of mongoid as of < 1.5 ... so I planned to do something no dependent of a model implementation
|
180
|
+
|
181
|
+
* [learn you some erlang?](http://learnyousomeerlang.com/higher-order-functions#maps-filters-folds), for the fold illustrations
|
182
|
+
|
183
|
+
* [Howard](http://rubyquiz.com/quiz67.html) and [Nunemaker](http://railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/) for inheritable class instance variables
|
184
|
+
|
185
|
+
|
186
|
+
Does it work ?
|
187
|
+
==============
|
188
|
+
|
189
|
+
specs are green on mri : [1.9.2-p0, 1.8.7-p302]
|
190
|
+
|
191
|
+
C0 coverage is acceptable under ruby-1.9.2-p0 (100% is acceptable for this kind of code)
|
192
|
+
|
193
|
+
There is few lines of code as concept is simple : fold a list of functions...
|
194
|
+
Though, we can make it better and lesser, cant we ?
|
195
|
+
|
196
|
+
|
197
|
+
Help|Contribute
|
198
|
+
===============
|
199
|
+
|
200
|
+
Fill an item in tracker
|
201
|
+
|
202
|
+
Add a page on wiki
|
203
|
+
|
204
|
+
Add a spec, open a pull request on topic branch, commit granted on first accepted patch|pull
|
data/Rakefile
ADDED
data/cant.gemspec
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "cant/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "cant"
|
7
|
+
s.version = Cant::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["thierry.henrio"]
|
10
|
+
s.email = ["thierry.henrio@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/thierryhenrio"
|
12
|
+
s.summary = %q{Tiny authorization library, let you craft your own rules}
|
13
|
+
s.description = %q{
|
14
|
+
include Cant
|
15
|
+
------------
|
16
|
+
class User; include Cant::Embeddable; end
|
17
|
+
|
18
|
+
class AuthorizationMiddleware; include Cant::Embeddable; end
|
19
|
+
|
20
|
+
declare rules
|
21
|
+
-------------
|
22
|
+
User.cant do |action=:update, post|
|
23
|
+
not post.user == self if Post === resource and action == :update
|
24
|
+
end
|
25
|
+
|
26
|
+
AuthorizationMiddleware.cant do |env|
|
27
|
+
not env['user'] == env['post'].user if env.path =~ /^\posts/ and env.method == 'PUT'
|
28
|
+
end
|
29
|
+
|
30
|
+
verify
|
31
|
+
------
|
32
|
+
user.cant? :update, post
|
33
|
+
user.die_if_cant! :update, post
|
34
|
+
|
35
|
+
control
|
36
|
+
-------
|
37
|
+
rescue_from Cant::AccessDenied do |error|
|
38
|
+
flash[:error] = error.message
|
39
|
+
redirect_to request.referer
|
40
|
+
end
|
41
|
+
}
|
42
|
+
|
43
|
+
s.rubyforge_project = "cant"
|
44
|
+
|
45
|
+
s.files = `git ls-files`.split("\n")
|
46
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
47
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
48
|
+
s.require_paths = ["lib"]
|
49
|
+
|
50
|
+
s.add_development_dependency 'rspec'
|
51
|
+
s.add_development_dependency 'wrong'
|
52
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'cant/engine'
|
2
|
+
module Cant
|
3
|
+
module Embeddable
|
4
|
+
# class instance variable can be helpful there
|
5
|
+
#
|
6
|
+
# http://rubyquiz.com/quiz67.html
|
7
|
+
# http://rubykoans.com/
|
8
|
+
#
|
9
|
+
# and there is code available, though I did not fully parsed it with enlightenment yet :)
|
10
|
+
# https://github.com/ahoward/fattr
|
11
|
+
#
|
12
|
+
# see also
|
13
|
+
# http://www.ruby-forum.com/topic/197051
|
14
|
+
# http://railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/
|
15
|
+
#
|
16
|
+
class << self
|
17
|
+
def included(base)
|
18
|
+
base.extend Cant::Editable
|
19
|
+
|
20
|
+
# XXX this sucks if class defines its own inherited callback and includes Cant::Embeddable
|
21
|
+
# double evil : order of inherited and extend statement has an effect : latter overwrite earlier !!!
|
22
|
+
class << base
|
23
|
+
def inherited(subclass)
|
24
|
+
subclass.rules.concat(rules)
|
25
|
+
[:die, :fold].each do |attr|
|
26
|
+
subclass.send(attr, &(send(attr)))
|
27
|
+
end
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
include Cant::Questionable
|
35
|
+
def cantfiguration
|
36
|
+
self.class
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/cant/engine.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
module Cant
|
2
|
+
module Editable
|
3
|
+
# list of Rules
|
4
|
+
# returns the list of rules for this device
|
5
|
+
def rules
|
6
|
+
unless @rules
|
7
|
+
@rules = []
|
8
|
+
@rules.concat(Cant.rules) unless self == Cant
|
9
|
+
end
|
10
|
+
@rules
|
11
|
+
end
|
12
|
+
# add a Rule, as a pair of functions {predicate, die}
|
13
|
+
#
|
14
|
+
# block form sets predicate function.
|
15
|
+
# block can have argument, (as any proc)
|
16
|
+
#
|
17
|
+
# eg :
|
18
|
+
# rooms = [:kitchen]
|
19
|
+
# cant {|context| rooms.include?(context[:room]) and context[:user]}
|
20
|
+
# cant {current_user.admin?}
|
21
|
+
#
|
22
|
+
# returns a rule which die function can be configured
|
23
|
+
#
|
24
|
+
# cant do |controller|
|
25
|
+
# controller.request.path =~ /admin/ unless controller.current_user.admin?
|
26
|
+
# end.die do |controller|
|
27
|
+
# raise AccessDenied.new(controller.request)
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# options form looks for the predicate and die functions as values in options,
|
31
|
+
# under symbols :predicate and :die
|
32
|
+
#
|
33
|
+
# cant :predicate => proc {|controller| not controller.current_user},
|
34
|
+
# :die => proc {|controller| controller.redirect '/users/sign_in'}
|
35
|
+
def cant(options={}, &block)
|
36
|
+
rule = if block.nil?
|
37
|
+
Rule.new(options[:predicate], options[:die] || self.die)
|
38
|
+
else
|
39
|
+
Rule.new(block, self.die)
|
40
|
+
end
|
41
|
+
rules << rule
|
42
|
+
rule
|
43
|
+
end
|
44
|
+
|
45
|
+
# set default die function for this device
|
46
|
+
#
|
47
|
+
# example :
|
48
|
+
# die do |request|
|
49
|
+
# raise AccessDenied, "Cant process #{request}"
|
50
|
+
# end
|
51
|
+
def die(&block)
|
52
|
+
@die = block unless block.nil?
|
53
|
+
@die || Cant.die
|
54
|
+
end
|
55
|
+
|
56
|
+
# define fold function
|
57
|
+
# fold function has arity 2..n
|
58
|
+
# - rules : the rules to traverse
|
59
|
+
# - receiver : a Questionable asking for cant?
|
60
|
+
# - *args : the arguments for each rule to pass to predicate functions
|
61
|
+
#
|
62
|
+
# returns a rule if strategy evaluates it cant do
|
63
|
+
# nil either
|
64
|
+
#
|
65
|
+
# eg :
|
66
|
+
# fold {true} #=> always cant
|
67
|
+
# fold {|rules, _receiver, context| rules.reverse.find {|rule| rule.predicate?(context)}}
|
68
|
+
def fold(&block)
|
69
|
+
@fold = block unless block.nil?
|
70
|
+
@fold || Cant.fold
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# module level configuration
|
75
|
+
# Cant.fold
|
76
|
+
# Cant.die
|
77
|
+
class << self
|
78
|
+
include Editable
|
79
|
+
end
|
80
|
+
# module level default values
|
81
|
+
fold {|rules, receiver, *args| Folds.first_rule_that_predicates_in_receiver(rules, receiver, *args)}
|
82
|
+
die {|*args| raise AccessDenied, "Cant you do that #{args}, can you ??"}
|
83
|
+
|
84
|
+
# questionable interface
|
85
|
+
module Questionable
|
86
|
+
attr_writer :cantfiguration
|
87
|
+
def cantfiguration
|
88
|
+
@cantfiguration ||= Object.new.extend(Editable)
|
89
|
+
end
|
90
|
+
# return strategy fold for rules with context (a rule or nil)
|
91
|
+
def cant?(*args)
|
92
|
+
cantfiguration.fold.call(cantfiguration.rules, self, *args)
|
93
|
+
end
|
94
|
+
# return evaled die function of strategy fold
|
95
|
+
def die_if_cant!(*args)
|
96
|
+
rule = cant?(*args)
|
97
|
+
rule.die!(*args) if rule
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# standalone engine
|
102
|
+
class Engine
|
103
|
+
include Editable
|
104
|
+
include Questionable
|
105
|
+
def cantfiguration
|
106
|
+
self
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# a Rule is a pair of functions :
|
111
|
+
# - predicate(*args), that return true if predicate is met (hint of predicate?)
|
112
|
+
# - die(*args), that cant raise if convenient
|
113
|
+
#
|
114
|
+
# this class could have been:
|
115
|
+
# -spared
|
116
|
+
# -an Array, with an optional syntactic sugar
|
117
|
+
class Rule
|
118
|
+
# a new rule with a predicate and response function
|
119
|
+
def initialize(predicate=nil, die=Cant.die)
|
120
|
+
@predicate=predicate
|
121
|
+
@die = die
|
122
|
+
end
|
123
|
+
# set or return predicate function using block
|
124
|
+
#
|
125
|
+
# return true means rule can die
|
126
|
+
#
|
127
|
+
# example :
|
128
|
+
# predicate do |request|
|
129
|
+
# not current_user.admin? if request.path =~ /^\/admin/
|
130
|
+
# end
|
131
|
+
def predicate(&block)
|
132
|
+
@predicate = block unless block.nil?
|
133
|
+
@predicate
|
134
|
+
end
|
135
|
+
# evaluates predicate function with args
|
136
|
+
def predicate?(*args)
|
137
|
+
predicate.call(*args)
|
138
|
+
end
|
139
|
+
# set die function using block
|
140
|
+
#
|
141
|
+
# example :
|
142
|
+
# die do |request|
|
143
|
+
# raise AccessDenied, "Cant process #{request}"
|
144
|
+
# end
|
145
|
+
def die(&block)
|
146
|
+
@die = block unless block.nil?
|
147
|
+
@die
|
148
|
+
end
|
149
|
+
# call die function with args
|
150
|
+
#
|
151
|
+
# *args - variable list of arguments
|
152
|
+
def die!(*args)
|
153
|
+
die.call(*args)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
module Folds
|
158
|
+
class << self
|
159
|
+
# first rule that predicates to true, all args are carried to closure
|
160
|
+
# this strategy does not use the receiver argument, and evaluate each predicate with the binding of its creation
|
161
|
+
def first_rule_that_predicates(rules, _receiver, *args)
|
162
|
+
rules.find {|rule| rule.predicate?(*args)}
|
163
|
+
end
|
164
|
+
# strategy that evals block in receiver context
|
165
|
+
# closure is rebound to receiver (acting as a function)
|
166
|
+
def first_rule_that_predicates_in_receiver(rules, receiver, *args)
|
167
|
+
rules.find do |rule|
|
168
|
+
receiver.instance_exec(*args, &(rule.predicate))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class AccessDenied < RuntimeError; end
|
175
|
+
end
|
data/lib/cant/version.rb
ADDED
data/lib/cant.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cant/embeddable'
|
3
|
+
|
4
|
+
describe Cant::Embeddable do
|
5
|
+
class Foo
|
6
|
+
def foo?; true; end
|
7
|
+
include Cant::Embeddable
|
8
|
+
cant {|x| 9<x if foo?}
|
9
|
+
end
|
10
|
+
class Bar < Foo
|
11
|
+
def foo?; false; end
|
12
|
+
def bar?; true; end
|
13
|
+
cant {|x| x>20 if bar?}
|
14
|
+
die {true}
|
15
|
+
end
|
16
|
+
describe 'including class' do
|
17
|
+
it 'can configured, and instance be queried' do
|
18
|
+
foo = Foo.new
|
19
|
+
deny {foo.cant?(9)}
|
20
|
+
assert {rescuing{foo.die_if_cant!(10)}.is_a? Cant::AccessDenied}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
describe 'subclass' do
|
24
|
+
# this does not work !!!
|
25
|
+
#
|
26
|
+
# it 'has base class callback called' do
|
27
|
+
# assert {bar.imposterized?}
|
28
|
+
# end
|
29
|
+
it 'has superclass and self rules' do
|
30
|
+
assert {Bar.rules.length == 2}
|
31
|
+
end
|
32
|
+
it 'subclass can use it' do
|
33
|
+
bar = Bar.new
|
34
|
+
bar.cant?(10)
|
35
|
+
assert {bar.cant?(21)}
|
36
|
+
end
|
37
|
+
# XXX this might be considered as a bug
|
38
|
+
it 'does not gain superclass rules when modified after class loading' do
|
39
|
+
Foo.rules << :whop
|
40
|
+
deny {Bar.rules.include? :whoop}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cant'
|
3
|
+
require 'stunts/stunt'
|
4
|
+
|
5
|
+
describe Cant do
|
6
|
+
it 'has fold default value to first_rule_that_predicates_in_receiver' do
|
7
|
+
Cant.cant {|x| include? x}
|
8
|
+
a = [1]
|
9
|
+
assert {Cant.fold.call(Cant.rules, a, 1)}
|
10
|
+
end
|
11
|
+
it 'has a raising die function' do
|
12
|
+
e = rescuing {Cant.die.call(:do, :that)}
|
13
|
+
assert {e.is_a? Cant::AccessDenied}
|
14
|
+
assert {e.message =~ /^Cant you do that.*\?$/}
|
15
|
+
end
|
16
|
+
it 'rules is enumerable' do
|
17
|
+
assert {Cant.rules.respond_to? :each}
|
18
|
+
end
|
19
|
+
after do
|
20
|
+
Cant.rules.clear
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
describe Cant::Editable do
|
26
|
+
let(:editable) {Object.new.extend Cant::Editable}
|
27
|
+
describe "#cant" do
|
28
|
+
it 'returns a Cant::Rule' do
|
29
|
+
rule = editable.cant {true}
|
30
|
+
assert {rule.is_a? Cant::Rule}
|
31
|
+
end
|
32
|
+
it 'accepts an option argument, that can provide both predicate and response functions' do
|
33
|
+
predicate, die = proc {:predicate}, proc {:die}
|
34
|
+
rule = editable.cant :predicate => predicate, :die => die
|
35
|
+
assert {rule.predicate == predicate}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#fold' do
|
40
|
+
context 'with a block' do
|
41
|
+
it 'sets the fold proc' do
|
42
|
+
editable.fold {:onoes}
|
43
|
+
assert {editable.fold.call == :onoes}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#die" do
|
49
|
+
before do
|
50
|
+
# XXX preserving default value for class instance variable
|
51
|
+
@proc=Cant.instance_variable_get(:@die)
|
52
|
+
Cant.die{2}
|
53
|
+
end
|
54
|
+
it 'provide default die function for this engine rules' do
|
55
|
+
editable.die{:im_not_dead}
|
56
|
+
assert {editable.cant.die.call == :im_not_dead}
|
57
|
+
end
|
58
|
+
it "returns top level response function as a fall case" do
|
59
|
+
assert {editable.cant.die.call == 2}
|
60
|
+
end
|
61
|
+
after do
|
62
|
+
Cant.instance_variable_set(:@die, @proc)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "rules" do
|
67
|
+
it 'cant does not creep in module rules' do
|
68
|
+
editable.cant {true}
|
69
|
+
deny {Cant.rules.include? editable.rules.first}
|
70
|
+
end
|
71
|
+
it 'has module rules first' do
|
72
|
+
Cant.rules << :first
|
73
|
+
assert {editable.rules == [:first]}
|
74
|
+
end
|
75
|
+
after do
|
76
|
+
Cant.rules.clear
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe Cant::Questionable do
|
82
|
+
include Cant::Questionable
|
83
|
+
# query
|
84
|
+
describe '#cant?' do
|
85
|
+
let(:admin) {Stunt.new(:admin => true)}
|
86
|
+
let(:user) {Stunt.new(:admin => false)}
|
87
|
+
|
88
|
+
before do
|
89
|
+
cantfiguration.cant{|context| context[:url] =~ /admin/ and context[:user].admin?}
|
90
|
+
end
|
91
|
+
it 'authorize admin on /admin/users' do
|
92
|
+
assert {cant?(:url => '/admin/users', :user => admin)}
|
93
|
+
end
|
94
|
+
it 'deny user on /admin/users' do
|
95
|
+
deny {cant?(:url => '/admin/users', :user => user)}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# query or die
|
100
|
+
describe '#die_if_cant!' do
|
101
|
+
it "return die function evaluation" do
|
102
|
+
cantfiguration.cant{true}.die{1}
|
103
|
+
assert {die_if_cant! == 1}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'cant be given a cantfiguration' do
|
108
|
+
self.cantfiguration = 1
|
109
|
+
assert{cantfiguration == 1}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe Cant::Rule do
|
114
|
+
let(:rule) {Cant::Rule.new}
|
115
|
+
describe "#die, #die!" do
|
116
|
+
it 'die! return call of die block' do
|
117
|
+
rule.die {1}
|
118
|
+
assert {rule.die! == 1}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe Cant::Folds do
|
124
|
+
describe "#first_rule_that_predicates" do
|
125
|
+
it 'carries all tailing args to closure (there is a first unused one)' do
|
126
|
+
rule = Cant::Rule.new(proc {|x,y| x+y==10})
|
127
|
+
deny {Cant::Folds.first_rule_that_predicates([rule], nil, 2, 7)}
|
128
|
+
assert {Cant::Folds.first_rule_that_predicates([rule], nil, 2, 8) == rule}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
describe "#first_rule_that_predicates_in_receiver" do
|
132
|
+
let(:receiver) {Stunt.new(:admin => true)}
|
133
|
+
it 'carry args to function evaled in receiver' do
|
134
|
+
rule = Cant::Rule.new(lambda {|x,y| admin? if (x+y == 2)})
|
135
|
+
assert {Cant::Folds.first_rule_that_predicates_in_receiver([rule], receiver, 1, 1)}
|
136
|
+
assert {Cant::Folds.first_rule_that_predicates_in_receiver([rule], receiver, 1, 1)}
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe Cant::Engine do
|
142
|
+
let(:engine) {Cant::Engine.new}
|
143
|
+
it 'can be configured and queried' do
|
144
|
+
engine.cant{|x,y,z| x+y != z}.die{'bad arith'}
|
145
|
+
deny {engine.cant?(1,2,3)}
|
146
|
+
assert {engine.die_if_cant!(1,2,4) == 'bad arith'}
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'models/user'
|
3
|
+
|
4
|
+
describe User do
|
5
|
+
before do
|
6
|
+
User.cant do |env|
|
7
|
+
env[:path] =~ /^\/admin/ unless admin?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context "without being an admin" do
|
12
|
+
let(:user) {User.new}
|
13
|
+
it 'can not access /admin ' do
|
14
|
+
assert {user.cant?(:path => '/admin')}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/models/user.rb
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- thierry.henrio
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-01-03 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :development
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: wrong
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :development
|
45
|
+
version_requirements: *id002
|
46
|
+
description: "\n include Cant\n ------------\n class User; include Cant::Embeddable; end\n \n class AuthorizationMiddleware; include Cant::Embeddable; end\n \n declare rules\n -------------\n User.cant do |action=:update, post|\n not post.user == self if Post === resource and action == :update\n end\n \n AuthorizationMiddleware.cant do |env|\n not env['user'] == env['post'].user if env.path =~ /^\\posts/ and env.method == 'PUT'\n end\n \n verify\n ------\n user.cant? :update, post\n user.die_if_cant! :update, post\n\n control\n -------\n rescue_from Cant::AccessDenied do |error|\n flash[:error] = error.message\n redirect_to request.referer\n end\n "
|
47
|
+
email:
|
48
|
+
- thierry.henrio@gmail.com
|
49
|
+
executables: []
|
50
|
+
|
51
|
+
extensions: []
|
52
|
+
|
53
|
+
extra_rdoc_files: []
|
54
|
+
|
55
|
+
files:
|
56
|
+
- .gitignore
|
57
|
+
- .rspec
|
58
|
+
- Gemfile
|
59
|
+
- Gemfile.lock
|
60
|
+
- Guardfile
|
61
|
+
- LICENSE.mit
|
62
|
+
- README.markdown
|
63
|
+
- Rakefile
|
64
|
+
- cant.gemspec
|
65
|
+
- lib/cant.rb
|
66
|
+
- lib/cant/embeddable.rb
|
67
|
+
- lib/cant/engine.rb
|
68
|
+
- lib/cant/version.rb
|
69
|
+
- spec/cant/embeddable_spec.rb
|
70
|
+
- spec/cant/engine_spec.rb
|
71
|
+
- spec/integration_spec.rb
|
72
|
+
- spec/models/user.rb
|
73
|
+
- spec/spec_helper.rb
|
74
|
+
- spec/stunts/stunt.rb
|
75
|
+
has_rdoc: true
|
76
|
+
homepage: https://github.com/thierryhenrio
|
77
|
+
licenses: []
|
78
|
+
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
segments:
|
90
|
+
- 0
|
91
|
+
version: "0"
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project: cant
|
103
|
+
rubygems_version: 1.3.7
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Tiny authorization library, let you craft your own rules
|
107
|
+
test_files:
|
108
|
+
- spec/cant/embeddable_spec.rb
|
109
|
+
- spec/cant/engine_spec.rb
|
110
|
+
- spec/integration_spec.rb
|
111
|
+
- spec/models/user.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
- spec/stunts/stunt.rb
|