rbacanable 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -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/Changes.rdoc ADDED
@@ -0,0 +1,141 @@
1
+ = RBACanable
2
+ === Canable (by John Nunemaker) with a Role Based Access System
3
+
4
+ == How the system used to work
5
+
6
+ The User class or whichever object needed to be authorized against an action would include <tt>Canable::Cans</tt>. Objects being manipulated that needed protection would include <tt>Canable::Ables</tt>, and you would override the <tt>viewable_by?(user)</tt> methods to implement the various permissions governing that resource and the user trying to access it.
7
+
8
+ == How RBACanable works now
9
+
10
+ RBACanable is significantly different than the original Canable and unfortunately not compatible with existing code that uses Canable. The original Canable was oriented so that each resource (an object including <tt>Canable::Able</tt>) managed it's own permissions. RBACanable is oriented in the opposite way, where each object that wants to preform an action and needs authorization manages the permissions that govern it. This is necessary because each one of these action-performing objects (called Actors in RBACanable) get their governing rules from a series of Roles (any module including <tt>Canable::Role</tt>).
11
+
12
+ == Actors
13
+
14
+ Whatever class(es) you want all permissions to run through should include <tt>Canable::Actor</tt> instad of <tt>Canable::Cans</tt>.
15
+
16
+ class User > ActiveRecord::Base
17
+ include Canable::Actor
18
+ default_role :employee
19
+ end
20
+
21
+ Actors represent objects in the system that want to preform an action. An actor's abilities are defined by the role(s) it plays.
22
+
23
+ == Roles
24
+
25
+ The rules that govern your actors are defined in modules extending <tt>Canable::Role</tt> and/or each other.
26
+
27
+ module EmployeeRole
28
+ include Canable::Role
29
+ default_response false
30
+ end
31
+
32
+ module ManagerRole
33
+ include Canable::Role
34
+ default_response true
35
+ end
36
+
37
+ == Ables
38
+
39
+ class Article
40
+ include MongoMapper::Document
41
+ include Canable::Ables
42
+ end
43
+
44
+ Including Canable::Ables adds the able methods to the class including it. In this instance, any article instance now has viewable_by?(actor), creatable_by?(actor), updatable_by?(actor) and destroyable_by?(actor). Now
45
+
46
+ 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.
47
+
48
+ class Article
49
+ include MongoMapper::Document
50
+ include Canable::Ables
51
+ userstamps! # adds creator and updater
52
+
53
+ def updatable_by?(user)
54
+ creator == user
55
+ end
56
+
57
+ def destroyable_by?(user)
58
+ updatable_by?(user)
59
+ end
60
+ end
61
+
62
+ Lets look at some sample code now:
63
+
64
+ john = User.create(:name => 'John')
65
+ steve = User.create(:name =. 'Steve')
66
+
67
+ ruby = Article.new(:title => 'Ruby')
68
+ john.can_create?(ruby) # true
69
+ steve.can_create?(ruby) # true
70
+
71
+ ruby.creator = john
72
+ ruby.save
73
+
74
+ john.can_view?(ruby) # true
75
+ steve.can_view?(ruby) # true
76
+
77
+ john.can_update?(ruby) # true
78
+ steve.can_update?(ruby) # false
79
+
80
+ john.can_destroy?(ruby) # true
81
+ steve.can_destroy?(ruby) # false
82
+
83
+ 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.
84
+
85
+ == Enforcers
86
+
87
+ class ApplicationController
88
+ include Canable::Enforcers
89
+ end
90
+
91
+ 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:
92
+
93
+ class ApplicationController
94
+ include Canable::Enforcers
95
+
96
+ delegate :can_view?, :to => :current_user
97
+ helper_method :can_view? # so you can use it in your views
98
+ hide_action :can_view?
99
+
100
+ private
101
+ def enforce_view_permission(resource)
102
+ raise Canable::Transgression unless can_view?(resource)
103
+ end
104
+ end
105
+
106
+ Which means you can use it like this:
107
+
108
+ class ArticlesController < ApplicationController
109
+ def show
110
+ @article = Article.find!(params[:id])
111
+ enforce_view_permission(@article)
112
+ end
113
+ end
114
+
115
+ 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.).
116
+
117
+ == Adding Your Own Actions
118
+
119
+ You can add your own actions like this:
120
+
121
+ Canable.add(:publish, :publishable)
122
+
123
+ The first parameter is the can method (ie: can_publish?) and the second is the able method (ie: publishable_by?).
124
+
125
+ == Review
126
+
127
+ 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.
128
+
129
+ == Note on Patches/Pull Requests
130
+
131
+ * Fork the project.
132
+ * Make your feature addition or bug fix.
133
+ * Add tests for it. This is important so I don't break it in a
134
+ future version unintentionally.
135
+ * Commit, do not mess with rakefile, version, or history.
136
+ (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)
137
+ * Send me a pull request. Bonus points for topic branches.
138
+
139
+ == Copyright
140
+
141
+ Copyright (c) 2010 John Nunemaker. See LICENSE for details.
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.
data/README.rdoc ADDED
@@ -0,0 +1,164 @@
1
+ = RBACanable
2
+ === Canable (by John Nunemaker) with a Role Based Access System latched on
3
+
4
+ Simple permissions system that features easily actors, resources for actors to manipulate, and roles actors play to define their permissions.
5
+
6
+
7
+ == Ables
8
+
9
+ Ables are the resource objets that an actor will want to manipulate.
10
+
11
+ class Article
12
+ include MongoMapper::Document
13
+ include Canable::Ables
14
+ end
15
+
16
+ Including Canable::Ables adds the able methods to the class including it. In this instance, any article instance now has viewable_by?(actor), creatable_by?(actor), updatable_by?(actor) and destroyable_by?(actor).
17
+
18
+ == Roles
19
+
20
+ The rules that govern your actors are defined in roles. Roles are modules that extend <tt>Canable::Role</tt> and/or other roles. Roles can be nested as deep and mixed and matched using Ruby's mixin system, as they are just normal Ruby modules that carry permission sets with them. The order they are included in each other will define the final permission set of the role: Roles included after (on a lower line) than others will override the settings of the others.
21
+ module Canable::Roles
22
+ module EmployeeRole
23
+ include Canable::Role
24
+ default_response false
25
+
26
+ def can_view_article?(article)
27
+ true
28
+ end
29
+
30
+ def can_update_article?(article)
31
+ article.owner == @name
32
+ end
33
+
34
+ def can_destroy_article?(article)
35
+ self.can_update_article?(article)
36
+ end
37
+ end
38
+
39
+ module ManagerRole
40
+ include Canable::Role
41
+ include EmployeeRole
42
+
43
+ def can_update_article?(article)
44
+ true
45
+ end
46
+
47
+ def can_destroy_article(article)
48
+ article.owner == @name
49
+ end
50
+ end
51
+ end
52
+
53
+ Here two roles are defined, one for employees and one for managers. Employees by default can't do anything, this is set by <tt>default_response false</tt> on the module. Then methods governing the actor's interaction with the Article model by defining the <tt>can_view_article?</tt> to always return true (Employees can view any article), and defining the <tt>can_update_article?</tt> method to only return true if the actor playing the employee role is the owner of the article.
54
+
55
+ The manager role includes the employee role, so it inherits the default_response behaviour of false and the defined rules for interactions with articles. Here, the <tt>can_update_article?</tt> method is override to always return true, so managers can edit any article. Note that <tt>EmployeeRole#can_destroy_article?(article)</tt> calls <tt>self.can_update_article?(article)</tt>, so when the ManagerRole inherits the EmployeeRole, <tt>ManagerRole#can_destroy_article?(article)</tt> returns the result of <tt>ManagerRole#can_update_article?(article)</tt>, which returns true. This means that when ManagerRole inherits EmployeeRole, the inherited methods behave differently, which might be unexpected. To mitigate this in the example above, <tt>ManagerRole#can_destroy_article?(article)</tt> is overridden to check if the Manager trying to destroy the role is the article owner.
56
+
57
+ == Actors
58
+
59
+ Whatever class(es) you want all permissions to run through should include <tt>Canable::Actor</tt>, instead of <tt>Canable::Cans</tt> from the original Canable gem.
60
+
61
+ class User > ActiveRecord::Base
62
+ include Canable::Actor
63
+ default_role :employee
64
+ end
65
+
66
+ Actors represent objects in the system that want to preform an action. An actor's abilities are defined by the role(s) it plays because the actor simply inherits any permission. Roles can be assigned to an actor ("played") by using the <tt>default_role(role)</tt> class method, or calling <tt>act(role)<tt> on an actor instance.
67
+
68
+
69
+ 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.
70
+
71
+ class Article
72
+ include MongoMapper::Document
73
+ include Canable::Ables
74
+ userstamps! # adds creator and updater
75
+
76
+ def updatable_by?(user)
77
+ creator == user
78
+ end
79
+
80
+ def destroyable_by?(user)
81
+ updatable_by?(user)
82
+ end
83
+ end
84
+
85
+ Lets look at some sample code now:
86
+
87
+ john = User.create(:name => 'John')
88
+ steve = User.create(:name =. 'Steve')
89
+
90
+ ruby = Article.new(:title => 'Ruby')
91
+ john.can_create?(ruby) # true
92
+ steve.can_create?(ruby) # true
93
+
94
+ ruby.creator = john
95
+ ruby.save
96
+
97
+ john.can_view?(ruby) # true
98
+ steve.can_view?(ruby) # true
99
+
100
+ john.can_update?(ruby) # true
101
+ steve.can_update?(ruby) # false
102
+
103
+ john.can_destroy?(ruby) # true
104
+ steve.can_destroy?(ruby) # false
105
+
106
+ 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.
107
+
108
+ == Enforcers
109
+
110
+ class ApplicationController
111
+ include Canable::Enforcers
112
+ end
113
+
114
+ 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:
115
+
116
+ class ApplicationController
117
+ include Canable::Enforcers
118
+
119
+ delegate :can_view?, :to => :current_user
120
+ helper_method :can_view? # so you can use it in your views
121
+ hide_action :can_view?
122
+
123
+ private
124
+ def enforce_view_permission(resource)
125
+ raise Canable::Transgression unless can_view?(resource)
126
+ end
127
+ end
128
+
129
+ Which means you can use it like this:
130
+
131
+ class ArticlesController < ApplicationController
132
+ def show
133
+ @article = Article.find!(params[:id])
134
+ enforce_view_permission(@article)
135
+ end
136
+ end
137
+
138
+ 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.).
139
+
140
+ == Adding Your Own Actions
141
+
142
+ You can add your own actions like this:
143
+
144
+ Canable.add(:publish, :publishable)
145
+
146
+ The first parameter is the can method (ie: can_publish?) and the second is the able method (ie: publishable_by?).
147
+
148
+ == Review
149
+
150
+ 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.
151
+
152
+ == Note on Patches/Pull Requests
153
+
154
+ * Fork the project.
155
+ * Make your feature addition or bug fix.
156
+ * Add tests for it. This is important so I don't break it in a
157
+ future version unintentionally.
158
+ * Commit, do not mess with rakefile, version, or history.
159
+ (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)
160
+ * Send me a pull request. Bonus points for topic branches.
161
+
162
+ == Copyright
163
+
164
+ Copyright (c) 2010 John Nunemaker. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
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 = "rbacanable"
10
+ gem.summary = %Q{Simple role based permissions system}
11
+ gem.description = %Q{Simple role based permissions system}
12
+ gem.email = "harry.brundage@gmail.com"
13
+ gem.homepage = "http://github.com/hornairs/rbacanable"
14
+ gem.authors = ["John Nunemaker", "Harry Brundage"]
15
+ gem.version = Canable::Version
16
+ gem.add_development_dependency "shoulda", "2.10.3"
17
+ gem.add_development_dependency "mocha", "0.9.8"
18
+ gem.add_development_dependency "yard", ">= 0"
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'rake/testtask'
27
+ Rake::TestTask.new(:test) do |test|
28
+ test.libs << 'lib' << 'test'
29
+ test.ruby_opts << '-rubygems'
30
+ test.pattern = 'test/**/test_*.rb'
31
+ test.verbose = true
32
+ end
33
+
34
+ task :test => :check_dependencies
35
+ task :default => :test
36
+
37
+ begin
38
+ require 'yard'
39
+ YARD::Rake::YardocTask.new
40
+ rescue LoadError
41
+ task :yardoc do
42
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
43
+ end
44
+ end
data/examples/basic.rb ADDED
@@ -0,0 +1,41 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rubygems'
4
+ require 'active_support'
5
+ require 'canable'
6
+
7
+ class User
8
+ include Canable::Cans
9
+ attr_accessor :name
10
+
11
+ def initialize(attributes = {})
12
+ @name = attributes[:name]
13
+ end
14
+
15
+ def can_update_article(article)
16
+ article.creator == @name
17
+ end
18
+ end
19
+
20
+ class Article
21
+ include Canable::Ables
22
+ attr_accessor :creator
23
+
24
+ def initialize(attributes = {})
25
+ @creator = attributes[:creator]
26
+ end
27
+ end
28
+
29
+ post1 = Article.new(:creator => "John")
30
+ user1 = User.new(:name => "John")
31
+ user2 = User.new(:name => "Steve")
32
+
33
+ puts "Can User1 view Post1? #{user1.can_view?(post1)}"
34
+ puts "Can User2 view Post1? #{user2.can_view?(post1)}"
35
+ puts "Is Post1 viewable by User1? #{post1.viewable_by?(user1)}"
36
+ puts "Is Post1 viewable by User2? #{post1.viewable_by?(user2)}"
37
+ puts
38
+ puts "Can User1 update Post1? #{user1.can_update?(post1)}"
39
+ puts "Can User2 update Post1? #{user2.can_update?(post1)}"
40
+ puts "Is Post1 updatable by User1? #{post1.updatable_by?(user1)}"
41
+ puts "Is Post1 updatable by User2? #{post1.updatable_by?(user2)}"
data/examples/roles.rb ADDED
@@ -0,0 +1,100 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rubygems'
4
+ require 'active_support'
5
+ require 'canable'
6
+ require 'terminal-table/import'
7
+
8
+
9
+ module Canable::Roles
10
+ module BasicRole
11
+ include Canable::Role
12
+ default_response false
13
+ end
14
+
15
+ module EmployeeRole
16
+ include Canable::Role
17
+ default_response false
18
+
19
+ def can_destroy_article?(article)
20
+ article.creator == @name
21
+ end
22
+
23
+ def can_update_article?(article)
24
+ article.creator == @name
25
+ end
26
+ end
27
+
28
+ module ManagerRole
29
+ include Canable::Role
30
+ include EmployeeRole
31
+
32
+ def can_destroy_article?(article)
33
+ true
34
+ end
35
+ end
36
+
37
+ module AdminRole
38
+ include Canable::Role
39
+ default_response true
40
+
41
+ def can_create_article?(article)
42
+ false
43
+ end
44
+ end
45
+ end
46
+
47
+ class User
48
+ include Canable::Actor
49
+ attr_accessor :name
50
+
51
+ default_role Canable::Roles::BasicRole
52
+ # role_attribute :@role -- RBACanable by default looks in @role for a Module constant or string to use as a role
53
+
54
+ def initialize(attributes = {})
55
+ @name = attributes[:name]
56
+ @role = attributes[:role]
57
+ self.__initialize_canable_role # nessecary since initialize is overridden
58
+ end
59
+
60
+ def to_s
61
+ "#{@role.to_s}: #{@name}"
62
+ end
63
+ end
64
+
65
+ class Article
66
+ include Canable::Ables
67
+ attr_accessor :creator
68
+
69
+ def initialize(attributes = {})
70
+ @creator = attributes[:creator]
71
+ end
72
+
73
+ def to_s
74
+ "#{@creator}'s article"
75
+ end
76
+ end
77
+
78
+ users = [
79
+ User.new(:name => "John", :role => :employee),
80
+ User.new(:name => "Steve", :role => :manager),
81
+ User.new(:name => "Harry", :role => :admin),
82
+ User.new(:name => "Jim")
83
+ ]
84
+
85
+ posts = [
86
+ Article.new(:creator => "John"),
87
+ Article.new(:creator => "Steve"),
88
+ Article.new(:creator => "Harry")
89
+ ]
90
+
91
+
92
+ user_table = table do |t|
93
+ t.headings = "User", "Resource", "Can View?", "Can Create?", "Can Update?", "Can Destroy?", "Role"
94
+ users.each do |user|
95
+ posts.each do |post|
96
+ t << [user.to_s, post.to_s, user.can_view?(post), user.can_create?(post), user.can_update?(post), user.can_destroy?(post), user.canable_included_role]
97
+ end
98
+ end
99
+ end
100
+ puts user_table