can_do 0.1.1 → 0.1.2
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/README +94 -0
- data/can_do-0.1.0.gem +0 -0
- data/can_do.gemspec +18 -0
- data/lib/can_do.rb +46 -0
- data/lib/can_do/dsl.rb +81 -0
- data/lib/can_do/permission_error.rb +8 -0
- data/spec/dsl_spec.rb +55 -0
- data/spec/spec_helper.rb +1 -0
- metadata +16 -8
data/README
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
Can Do
|
2
|
+
|
3
|
+
DSL-based permission rules for Rails
|
4
|
+
|
5
|
+
Example:
|
6
|
+
|
7
|
+
class Permission
|
8
|
+
def self.define_rules
|
9
|
+
CanDo.setup do
|
10
|
+
can :index, User do
|
11
|
+
rule("You must be logged in.") {User.current}
|
12
|
+
rule("You must have an active account to do this.") {User.current.active?}
|
13
|
+
end
|
14
|
+
|
15
|
+
can :show, User do
|
16
|
+
cascade :index #inherit the logged in and active rules from :index
|
17
|
+
rule("You may not view others' accounts if they are private.") do |user|
|
18
|
+
!user.private? || user == User.current || User.current.admin?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
can :update, User do
|
23
|
+
cascade :index #inherit the logged in and active rules from :index
|
24
|
+
rule("You may not update others' accounts.") {|user| user == User.current || User.current.admin?}
|
25
|
+
end
|
26
|
+
|
27
|
+
can :delete, User do
|
28
|
+
cascade :update
|
29
|
+
end
|
30
|
+
|
31
|
+
can :create, UserInterest do
|
32
|
+
# You may create an interest if you have permission to update that user.
|
33
|
+
cascade :update, {|interest| interest.user}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
application.rb:
|
40
|
+
ActionDispatch::Callbacks.to_prepare do
|
41
|
+
Permission.define_rules #allows rules to be reloaded when classes are reloaded
|
42
|
+
end
|
43
|
+
|
44
|
+
users_controller.rb:
|
45
|
+
def index
|
46
|
+
require_permission! :index, User #this will raise a CanDo::PermissionError if permission is denied
|
47
|
+
...
|
48
|
+
end
|
49
|
+
|
50
|
+
def show
|
51
|
+
@user = User.find(params[:id])
|
52
|
+
require_permission! :show, user #this will raise a CanDo::PermissionError if permission is denied
|
53
|
+
...
|
54
|
+
end
|
55
|
+
|
56
|
+
application_controller.rb:
|
57
|
+
rescue_from CanDo::PermissionError do |error|
|
58
|
+
render :text => "Permission denied: #{error.message}"
|
59
|
+
end
|
60
|
+
|
61
|
+
before_filter :initialize_current_user
|
62
|
+
|
63
|
+
def initialize_current_user
|
64
|
+
User.current = your_code_goes_here
|
65
|
+
end
|
66
|
+
|
67
|
+
user.rb
|
68
|
+
def self.current=(value)
|
69
|
+
Thread.current["User.current"] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.current
|
73
|
+
Thread.current["User.current"]
|
74
|
+
end
|
75
|
+
|
76
|
+
users/index.haml
|
77
|
+
%ul
|
78
|
+
- @users.each do |user|
|
79
|
+
- can?(:show, user) do
|
80
|
+
%li
|
81
|
+
= link_to user.name, user_path(user)
|
82
|
+
- if can?(:update, user) do
|
83
|
+
= link_to "Edit", edit_user_path(user)
|
84
|
+
- can?(:create, User) do
|
85
|
+
= link_to "Add User", new_user_path
|
86
|
+
|
87
|
+
|
88
|
+
To test your permission logic, simply call CanDo.reason(:verb, object) and test that the reason is what you expect. Make sure to test all rules inherited from cascades as well. Without this, it's easy for cascades to introduce unintended consequences.
|
89
|
+
|
90
|
+
|
91
|
+
Special thanks to cancan, upon which Can Do is loosely based. Important differences:
|
92
|
+
* For large permission sets, cancan slows down dramatically. Can Do uses hash-based lookups, which dramatically reduces performance overhead.
|
93
|
+
* Can Do is far more expressive, allowing user-friendly explanations for failures.
|
94
|
+
* Can Do has explicit support for cascading rules to reduce repetition.
|
data/can_do-0.1.0.gem
ADDED
Binary file
|
data/can_do.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "can_do"
|
6
|
+
s.version = "0.1.2"
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Ryan Fogle", "Zachary Kurmas"]
|
9
|
+
s.email = "nayrelgof@gmail.com"
|
10
|
+
s.homepage = "http://github.com/fogle/can_do"
|
11
|
+
s.summary = "DSL-based permission rules for Rails"
|
12
|
+
s.description = "DSL-based permission rules for easy authorization logic in Rails"
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
end
|
data/lib/can_do.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'can_do/dsl'
|
2
|
+
require 'can_do/permission_error'
|
3
|
+
|
4
|
+
module CanDo
|
5
|
+
def self.setup &block
|
6
|
+
@active_dsl = CanDo::Dsl.new &block
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.can?(verb, noun, &block)
|
10
|
+
raise "you must first call setup" unless @active_dsl
|
11
|
+
@active_dsl.can?(verb, noun, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.reason(verb, noun)
|
15
|
+
raise "you must first call setup" unless @active_dsl
|
16
|
+
@active_dsl.reason(verb, noun)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ActionController
|
21
|
+
class Base
|
22
|
+
def can?(*args, &block)
|
23
|
+
CanDo.can?(*args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def require_permission!(*args)
|
27
|
+
reason = CanDo.reason(*args)
|
28
|
+
unless reason.nil?
|
29
|
+
Rails.logger.info "Permission Error: #{reason}"
|
30
|
+
deny_permission(reason, *args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def deny_permission(reason, *args)
|
35
|
+
raise CanDo::PermissionError.new(reason)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module ActionView
|
41
|
+
module Helpers
|
42
|
+
def can?(*args, &block)
|
43
|
+
CanDo.can?(*args, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/can_do/dsl.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module CanDo
|
2
|
+
class Dsl
|
3
|
+
NIL_REASON = "Object of query does not exist."
|
4
|
+
def noun(object)
|
5
|
+
noun = object.class unless object.class == Class || object.class == String
|
6
|
+
noun || object
|
7
|
+
end
|
8
|
+
|
9
|
+
def can?(action, object)
|
10
|
+
can = internal_reason(action, noun(object), object).nil?
|
11
|
+
yield if can && block_given?
|
12
|
+
can
|
13
|
+
end
|
14
|
+
|
15
|
+
def reason(action, object)
|
16
|
+
internal_reason(action, noun(object), object)
|
17
|
+
end
|
18
|
+
|
19
|
+
def internal_reason(action, noun, object)
|
20
|
+
return NIL_REASON if object.nil?
|
21
|
+
rules = @can_hash[action][noun]
|
22
|
+
rules.each do |rule|
|
23
|
+
return rule.reason unless rule.call(object, noun)
|
24
|
+
end
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def can(action, noun, &block)
|
29
|
+
@rules = []
|
30
|
+
yield
|
31
|
+
@can_hash[action] ||= {}
|
32
|
+
@can_hash[action][noun] = @rules
|
33
|
+
end
|
34
|
+
|
35
|
+
def rule(reason=nil, &block)
|
36
|
+
@rules << Rule.new(reason, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def cascade(action, &block)
|
40
|
+
@rules << Cascade.new(action, self, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize &block
|
44
|
+
@can_hash = {}
|
45
|
+
instance_eval &block
|
46
|
+
end
|
47
|
+
|
48
|
+
class Rule
|
49
|
+
attr_reader :reason
|
50
|
+
|
51
|
+
def initialize(reason, &block)
|
52
|
+
@reason = reason || "Unknown Reason"
|
53
|
+
@block = block
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(object, noun)
|
57
|
+
@block.call(object)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Cascade
|
62
|
+
def initialize(action, ability, &block)
|
63
|
+
@action = action
|
64
|
+
@ability = ability
|
65
|
+
@transformation = block
|
66
|
+
end
|
67
|
+
|
68
|
+
def reason
|
69
|
+
@reason
|
70
|
+
end
|
71
|
+
|
72
|
+
def call(object, noun)
|
73
|
+
@reason = nil
|
74
|
+
object = @transformation.call(object) if @transformation
|
75
|
+
noun = @ability.noun(object) if @transformation
|
76
|
+
@reason = @ability.internal_reason(@action, noun, object)
|
77
|
+
@reason.nil?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/spec/dsl_spec.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CanDo::Dsl do
|
4
|
+
before do
|
5
|
+
@dsl = CanDo::Dsl.new
|
6
|
+
@dsl.instance_eval do
|
7
|
+
can :be_numeric, Symbol do
|
8
|
+
rule("couldn't convert to a number") {|symbol, noun| symbol.to_s.gsub('"', '').to_i.to_s == symbol.to_s.gsub('"', '')}
|
9
|
+
end
|
10
|
+
|
11
|
+
can :be_odd, Fixnum do
|
12
|
+
rule("wasn't odd") {|number, noun| number.odd?}
|
13
|
+
end
|
14
|
+
|
15
|
+
can :be_odd, Symbol do
|
16
|
+
cascade(:be_numeric)
|
17
|
+
cascade(:be_odd) {|symbol| symbol.to_s.gsub('"', '').to_i}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'returns non-nil when passed a nil noun' do
|
23
|
+
@dsl.can?(:be_numeric, nil).should == false
|
24
|
+
@dsl.reason(:be_numeric, nil).should == CanDo::Dsl::NIL_REASON
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'handles simple rules like :be_numeric, Symbol' do
|
28
|
+
@dsl.can?(:be_numeric, :"3").should == true
|
29
|
+
@dsl.can?(:be_numeric, :"3ish").should == false
|
30
|
+
|
31
|
+
|
32
|
+
@dsl.reason(:be_numeric, :"3").should be_nil
|
33
|
+
@dsl.reason(:be_numeric, :"3ish").should == "couldn't convert to a number"
|
34
|
+
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'handles simple rules like :be_odd, Fixnum' do
|
39
|
+
@dsl.can?(:be_odd, 3).should == true
|
40
|
+
@dsl.can?(:be_odd, 2).should == false
|
41
|
+
@dsl.reason(:be_odd, 3).should be_nil
|
42
|
+
@dsl.reason(:be_odd, 2).should == "wasn't odd"
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'supports cascading rules' do
|
46
|
+
@dsl.can?(:be_odd, :"3ish").should == false
|
47
|
+
@dsl.reason(:be_odd, :"3ish").should == "couldn't convert to a number"
|
48
|
+
|
49
|
+
@dsl.can?(:be_odd, :"3").should == true
|
50
|
+
@dsl.reason(:be_odd, :"3").should be_nil
|
51
|
+
|
52
|
+
@dsl.can?(:be_odd, :"2").should == false
|
53
|
+
@dsl.reason(:be_odd, :"2").should == "wasn't odd"
|
54
|
+
end
|
55
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'can_do'
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: can_do
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 31
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 2
|
10
|
+
version: 0.1.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ryan Fogle
|
@@ -27,9 +27,16 @@ extensions: []
|
|
27
27
|
|
28
28
|
extra_rdoc_files: []
|
29
29
|
|
30
|
-
files:
|
31
|
-
|
32
|
-
|
30
|
+
files:
|
31
|
+
- README
|
32
|
+
- can_do-0.1.0.gem
|
33
|
+
- can_do.gemspec
|
34
|
+
- lib/can_do.rb
|
35
|
+
- lib/can_do/dsl.rb
|
36
|
+
- lib/can_do/permission_error.rb
|
37
|
+
- spec/dsl_spec.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
homepage: http://github.com/fogle/can_do
|
33
40
|
licenses: []
|
34
41
|
|
35
42
|
post_install_message:
|
@@ -62,5 +69,6 @@ rubygems_version: 1.7.2
|
|
62
69
|
signing_key:
|
63
70
|
specification_version: 3
|
64
71
|
summary: DSL-based permission rules for Rails
|
65
|
-
test_files:
|
66
|
-
|
72
|
+
test_files:
|
73
|
+
- spec/dsl_spec.rb
|
74
|
+
- spec/spec_helper.rb
|