canable 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ tmp
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 John Nunemaker
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,122 @@
1
+ = Canable
2
+
3
+ Simple permissions that I have used on my last several projects so I figured it was time to abstract and wrap up into something more easily reusable.
4
+
5
+ == Cans
6
+
7
+ Whatever class you want all permissions to run through should include Canable::Cans.
8
+
9
+ class User
10
+ include MongoMapper::Document
11
+ include Canable::Cans
12
+ end
13
+
14
+ This means that an instance of a user automatically gets can methods for the default REST actions: can_view?(resource), can_create?(resource), can_update?(resource), can_destroy?(resource).
15
+
16
+ == Ables
17
+
18
+ Each of the can methods simply calls the related "able" method (viewable, creatable, updatable, destroyable) for the action (view, create, update, delete). Canable comes with defaults for this methods that you can then override as makes sense for your permissions.
19
+
20
+ class Article
21
+ include MongoMapper::Document
22
+ include Canable::Ables
23
+ end
24
+
25
+ Including Canable::Ables adds the able methods to the class including it. In this instance, article now has viewable_by?(user), creatable_by?(user), updatable_by?(user) and destroyable_by?(user).
26
+
27
+ Lets say an article can be viewed and created by anyone, but only updated or destroyed by the user that created the article. To do that, you could leave viewable_by? and creatable_by? alone as they default to true and just override the other methods.
28
+
29
+ class Article
30
+ include MongoMapper::Document
31
+ include Canable::Ables
32
+ userstamps! # adds creator and updater
33
+
34
+ def updatable_by?(user)
35
+ creator == user
36
+ end
37
+
38
+ def destroyable_by?(user)
39
+ updatable_by?(user)
40
+ end
41
+ end
42
+
43
+ Lets look at some sample code now:
44
+
45
+ john = User.create(:name => 'John')
46
+ steve = User.create(:name =. 'Steve')
47
+
48
+ ruby = Article.new(:title => 'Ruby')
49
+ john.can_create?(ruby) # true
50
+ steve.can_create?(ruby) # true
51
+
52
+ ruby.creator = john
53
+ ruby.save
54
+
55
+ john.can_view?(ruby) # true
56
+ steve.can_view?(ruby) # true
57
+
58
+ john.can_update?(ruby) # true
59
+ steve.can_update?(ruby) # false
60
+
61
+ john.can_destroy?(ruby) # true
62
+ steve.can_destroy?(ruby) # false
63
+
64
+ Now we can implement our permissions for each resource and then always check whether a user can or cannot do something. This makes it all really easy to test. Next, how would you use this in the controller.
65
+
66
+ == Enforcers
67
+
68
+ class ApplicationController
69
+ include Canable::Enforcers
70
+ end
71
+
72
+ Including Canable::Enforcers adds an enforce permission method for each of the actions defined (by default view/create/update/destroy). It is the same thing as doing this for each Canable action:
73
+
74
+ class ApplicationController
75
+ include Canable::Enforcers
76
+
77
+ delegate :can_view?, :to => :current_user
78
+ helper_method :can_view? # so you can use it in your views
79
+ hide_action :can_view?
80
+
81
+ private
82
+ def enforce_view_permission(resource)
83
+ raise Canable::Transgression unless can_view?(resource)
84
+ end
85
+ end
86
+
87
+ Which means you can use it like this:
88
+
89
+ class ArticlesController < ApplicationController
90
+ def show
91
+ @article = Article.find!(params[:id])
92
+ enforce_view_permission(@article)
93
+ end
94
+ end
95
+
96
+ If the user can_view? the article, all is well. If not, a Canable::Transgression is raised which you can decide how to handle (show 404, slap them on the wrist, etc.).
97
+
98
+ == Adding Your Own Actions
99
+
100
+ You can add your own actions like this:
101
+
102
+ Canable.add(:publish, :publishable)
103
+
104
+ The first parameter is the can method (ie: can_publish?) and the second is the able method (ie: publishable_by?).
105
+
106
+ == Review
107
+
108
+ So, lets review: cans go on user model, ables go on everything, you override ables in each model where you want to enforce permissions, and enforcers go after each time you find or initialize an object in a controller. Bing, bang, boom.
109
+
110
+ == Note on Patches/Pull Requests
111
+
112
+ * Fork the project.
113
+ * Make your feature addition or bug fix.
114
+ * Add tests for it. This is important so I don't break it in a
115
+ future version unintentionally.
116
+ * Commit, do not mess with rakefile, version, or history.
117
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
118
+ * Send me a pull request. Bonus points for topic branches.
119
+
120
+ == Copyright
121
+
122
+ Copyright (c) 2010 John Nunemaker. See LICENSE for details.
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require File.dirname(__FILE__) + '/lib/canable'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "canable"
10
+ gem.summary = %Q{Simple permissions that I have used on my last several projects so I figured it was time to abstract and wrap up into something more easily reusable.}
11
+ gem.description = %Q{Simple permissions that I have used on my last several projects so I figured it was time to abstract and wrap up into something more easily reusable.}
12
+ gem.email = "nunemaker@gmail.com"
13
+ gem.homepage = "http://github.com/jnunemaker/canable"
14
+ gem.authors = ["John Nunemaker"]
15
+ gem.version = Canable::Version
16
+ gem.add_development_dependency "shoulda", "2.10.2"
17
+ gem.add_development_dependency "mongo_mapper", "0.7"
18
+ gem.add_development_dependency "mocha", "0.9.8"
19
+ gem.add_development_dependency "yard", ">= 0"
20
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
25
+ end
26
+
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.ruby_opts << '-rubygems'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ task :test => :check_dependencies
36
+ task :default => :test
37
+
38
+ begin
39
+ require 'yard'
40
+ YARD::Rake::YardocTask.new
41
+ rescue LoadError
42
+ task :yardoc do
43
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
44
+ end
45
+ end
@@ -0,0 +1,77 @@
1
+ module Canable
2
+ Version = '0.1'
3
+
4
+ # Module that holds all the can_action? methods.
5
+ module Cans; end
6
+
7
+ # Module that holds all the [method]able_by? methods.
8
+ module Ables; end
9
+
10
+ # Module that holds all the enforce_[action]_permission methods for use in controllers.
11
+ module Enforcers
12
+ def self.included(controller)
13
+ controller.class_eval do
14
+ Canable.actions.each do |can, able|
15
+ delegate "can_#{can}?", :to => :current_user
16
+ helper_method "can_#{can}?" if controller.respond_to?(:helper_method)
17
+ hide_action "can_#{can}?" if controller.respond_to?(:hide_action)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Exception that gets raised when permissions are broken for whatever reason.
24
+ class Transgression < StandardError; end
25
+
26
+ # Default actions to an empty hash.
27
+ @actions = {}
28
+
29
+ # Returns hash of actions that have been added.
30
+ # {:view => :viewable, ...}
31
+ def self.actions
32
+ @actions
33
+ end
34
+
35
+ # Adds an action to actions and the correct methods to can and able modules.
36
+ #
37
+ # @param [Symbol] can_method The name of the can_[action]? method.
38
+ # @param [Symbol] resource_method The name of the [resource_method]_by? method.
39
+ def self.add(can, able)
40
+ @actions[can] = able
41
+ add_can_method(can, able)
42
+ add_able_method(able)
43
+ add_enforcer_method(can)
44
+ end
45
+
46
+ private
47
+ def self.add_can_method(can, able)
48
+ Cans.module_eval <<-EOM
49
+ def can_#{can}?(resource)
50
+ return false if resource.blank?
51
+ resource.#{able}_by?(self)
52
+ end
53
+ EOM
54
+ end
55
+
56
+ def self.add_able_method(able)
57
+ Ables.module_eval <<-EOM
58
+ def #{able}_by?(user)
59
+ true
60
+ end
61
+ EOM
62
+ end
63
+
64
+ def self.add_enforcer_method(can)
65
+ Enforcers.module_eval <<-EOM
66
+ def enforce_#{can}_permission(resource)
67
+ raise Canable::Transgression unless can_#{can}?(resource)
68
+ end
69
+ private :enforce_#{can}_permission
70
+ EOM
71
+ end
72
+ end
73
+
74
+ Canable.add(:view, :viewable)
75
+ Canable.add(:create, :creatable)
76
+ Canable.add(:update, :updatable)
77
+ Canable.add(:destroy, :destroyable)
@@ -0,0 +1,47 @@
1
+ def growl(title, msg, img)
2
+ %x{growlnotify -m #{ msg.inspect} -t #{title.inspect} --image ~/.watchr/#{img}.png}
3
+ end
4
+
5
+ def form_growl_message(str)
6
+ results = str.split("\n").last
7
+ if results =~ /[1-9]\s(failure|error)s?/
8
+ growl "Test Results", "#{results}", "fail"
9
+ elsif results != ""
10
+ growl "Test Results", "#{results}", "pass"
11
+ end
12
+ end
13
+
14
+ def run(cmd)
15
+ puts(cmd)
16
+ output = ""
17
+ IO.popen(cmd) do |com|
18
+ com.each_char do |c|
19
+ print c
20
+ output << c
21
+ $stdout.flush
22
+ end
23
+ end
24
+ form_growl_message output
25
+ end
26
+
27
+ def run_test_file(file)
28
+ run %Q(ruby -I"lib:test" -rubygems #{file})
29
+ end
30
+
31
+ def run_all_tests
32
+ run "rake test"
33
+ end
34
+
35
+ watch('test/helper\.rb') { system('clear'); run_all_tests }
36
+ watch('test/test_.*\.rb') { |m| system('clear'); run_test_file(m[0]) }
37
+ watch('lib/.*') { |m| system('clear'); run_all_tests }
38
+
39
+ # Ctrl-\
40
+ Signal.trap('QUIT') do
41
+ puts " --- Running all tests ---\n\n"
42
+ run_all_tests
43
+ end
44
+
45
+ # Ctrl-C
46
+ Signal.trap('INT') { abort("\n") }
47
+
@@ -0,0 +1,38 @@
1
+ require 'test/unit'
2
+
3
+ gem 'mocha', '0.9.8'
4
+ gem 'shoulda', '2.10.2'
5
+ gem 'mongo_mapper', '0.7'
6
+
7
+ require 'mocha'
8
+ require 'shoulda'
9
+ require 'mongo_mapper'
10
+
11
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
12
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
13
+ require 'canable'
14
+
15
+ class Test::Unit::TestCase
16
+ end
17
+
18
+ def Doc(name=nil, &block)
19
+ klass = Class.new do
20
+ include MongoMapper::Document
21
+ set_collection_name "test#{rand(20)}"
22
+
23
+ if name
24
+ class_eval "def self.name; '#{name}' end"
25
+ class_eval "def self.to_s; '#{name}' end"
26
+ end
27
+ end
28
+
29
+ klass.class_eval(&block) if block_given?
30
+ klass.collection.remove
31
+ klass
32
+ end
33
+
34
+ test_dir = File.expand_path(File.dirname(__FILE__) + '/../tmp')
35
+ FileUtils.mkdir_p(test_dir) unless File.exist?(test_dir)
36
+
37
+ MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017, {:logger => Logger.new(test_dir + '/test.log')})
38
+ MongoMapper.database = 'test'
@@ -0,0 +1,51 @@
1
+ require 'helper'
2
+
3
+ class AblesTest < Test::Unit::TestCase
4
+ context "Class with Canable::Ables included" do
5
+ setup do
6
+ klass = Doc do
7
+ include Canable::Ables
8
+ end
9
+
10
+ @resource = klass.new
11
+ @user = mock('user')
12
+ end
13
+
14
+ should "default viewable_by? to true" do
15
+ assert @resource.viewable_by?(@user)
16
+ end
17
+
18
+ should "default creatable_by? to true" do
19
+ assert @resource.creatable_by?(@user)
20
+ end
21
+
22
+ should "default updatable_by? to true" do
23
+ assert @resource.updatable_by?(@user)
24
+ end
25
+
26
+ should "default destroyable_by? to true" do
27
+ assert @resource.destroyable_by?(@user)
28
+ end
29
+ end
30
+
31
+ context "Class that overrides an able method" do
32
+ setup do
33
+ klass = Doc do
34
+ include Canable::Ables
35
+
36
+ def viewable_by?(user)
37
+ user.name == 'John'
38
+ end
39
+ end
40
+
41
+ @resource = klass.new
42
+ @john = mock('user', :name => 'John')
43
+ @steve = mock('user', :name => 'Steve')
44
+ end
45
+
46
+ should "use the overriden method and not default to true" do
47
+ assert @resource.viewable_by?(@john)
48
+ assert ! @resource.viewable_by?(@steve)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ require 'helper'
2
+
3
+ class TestCanable < Test::Unit::TestCase
4
+ context "Canable" do
5
+ should "have view action by default" do
6
+ assert_equal :viewable, Canable.actions[:view]
7
+ end
8
+
9
+ should "have create action by default" do
10
+ assert_equal :creatable, Canable.actions[:create]
11
+ end
12
+
13
+ should "have update action by default" do
14
+ assert_equal :updatable, Canable.actions[:update]
15
+ end
16
+
17
+ should "have destroy action by default" do
18
+ assert_equal :destroyable, Canable.actions[:destroy]
19
+ end
20
+
21
+ should "be able to add another action" do
22
+ Canable.add(:publish, :publishable)
23
+ assert_equal :publishable, Canable.actions[:publish]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
1
+ require 'helper'
2
+
3
+ class CansTest < Test::Unit::TestCase
4
+ context "Class with Canable::Cans included" do
5
+ setup do
6
+ klass = Doc do
7
+ include Canable::Cans
8
+ end
9
+
10
+ @user = klass.create(:name => 'John')
11
+ end
12
+
13
+ context "can_view?" do
14
+ should "be true if resource is viewable_by?" do
15
+ resource = mock('resource', :viewable_by? => true)
16
+ assert @user.can_view?(resource)
17
+ end
18
+
19
+ should "be false if resource is not viewable_by?" do
20
+ resource = mock('resource', :viewable_by? => false)
21
+ assert ! @user.can_view?(resource)
22
+ end
23
+
24
+ should "be false if resource is blank" do
25
+ assert ! @user.can_view?(nil)
26
+ assert ! @user.can_view?('')
27
+ end
28
+ end
29
+
30
+ context "can_create?" do
31
+ should "be true if resource is creatable_by?" do
32
+ resource = mock('resource', :creatable_by? => true)
33
+ assert @user.can_create?(resource)
34
+ end
35
+
36
+ should "be false if resource is not creatable_by?" do
37
+ resource = mock('resource', :creatable_by? => false)
38
+ assert ! @user.can_create?(resource)
39
+ end
40
+
41
+ should "be false if resource is blank" do
42
+ assert ! @user.can_create?(nil)
43
+ assert ! @user.can_create?('')
44
+ end
45
+ end
46
+
47
+ context "can_update?" do
48
+ should "be true if resource is updatable_by?" do
49
+ resource = mock('resource', :updatable_by? => true)
50
+ assert @user.can_update?(resource)
51
+ end
52
+
53
+ should "be false if resource is not updatable_by?" do
54
+ resource = mock('resource', :updatable_by? => false)
55
+ assert ! @user.can_update?(resource)
56
+ end
57
+
58
+ should "be false if resource is blank" do
59
+ assert ! @user.can_update?(nil)
60
+ assert ! @user.can_update?('')
61
+ end
62
+ end
63
+
64
+ context "can_destroy?" do
65
+ should "be true if resource is destroyable_by?" do
66
+ resource = mock('resource', :destroyable_by? => true)
67
+ assert @user.can_destroy?(resource)
68
+ end
69
+
70
+ should "be false if resource is not destroyable_by?" do
71
+ resource = mock('resource', :destroyable_by? => false)
72
+ assert ! @user.can_destroy?(resource)
73
+ end
74
+
75
+ should "be false if resource is blank" do
76
+ assert ! @user.can_destroy?(nil)
77
+ assert ! @user.can_destroy?('')
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,32 @@
1
+ require 'helper'
2
+
3
+ class EnforcersTest < Test::Unit::TestCase
4
+ context "Including Canable::Enforcers in a class" do
5
+ setup do
6
+ klass = Class.new do
7
+ include Canable::Enforcers
8
+ attr_accessor :current_user, :article
9
+
10
+ def show
11
+ enforce_view_permission(article)
12
+ end
13
+ end
14
+
15
+ @article = mock('article')
16
+ @user = mock('user')
17
+ @controller = klass.new
18
+ @controller.article = @article
19
+ @controller.current_user = @user
20
+ end
21
+
22
+ should "not raise error if can" do
23
+ @user.expects(:can_view?).with(@article).returns(true)
24
+ assert_nothing_raised { @controller.show }
25
+ end
26
+
27
+ should "raise error if cannot" do
28
+ @user.expects(:can_view?).with(@article).returns(false)
29
+ assert_raises(Canable::Transgression) { @controller.show }
30
+ end
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: canable
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - John Nunemaker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-27 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.10.2
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: mongo_mapper
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "="
32
+ - !ruby/object:Gem::Version
33
+ version: "0.7"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: mocha
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.9.8
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: yard
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ description: Simple permissions that I have used on my last several projects so I figured it was time to abstract and wrap up into something more easily reusable.
56
+ email: nunemaker@gmail.com
57
+ executables: []
58
+
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - LICENSE
63
+ - README.rdoc
64
+ files:
65
+ - .document
66
+ - .gitignore
67
+ - LICENSE
68
+ - README.rdoc
69
+ - Rakefile
70
+ - lib/canable.rb
71
+ - specs.watchr
72
+ - test/helper.rb
73
+ - test/test_ables.rb
74
+ - test/test_canable.rb
75
+ - test/test_cans.rb
76
+ - test/test_enforcers.rb
77
+ has_rdoc: true
78
+ homepage: http://github.com/jnunemaker/canable
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options:
83
+ - --charset=UTF-8
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ version:
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.3.5
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Simple permissions that I have used on my last several projects so I figured it was time to abstract and wrap up into something more easily reusable.
105
+ test_files:
106
+ - test/helper.rb
107
+ - test/test_ables.rb
108
+ - test/test_canable.rb
109
+ - test/test_cans.rb
110
+ - test/test_enforcers.rb