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