can_do 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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.
Binary file
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ module CanDo
2
+ class PermissionError < Exception
3
+ def initialize(reason, debug_info = nil)
4
+ super(reason)
5
+ @debug_info = debug_info
6
+ end
7
+ end
8
+ end
@@ -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
@@ -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: 25
4
+ hash: 31
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 1
10
- version: 0.1.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
- homepage:
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