walruz 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2009 Roman Gonzalez <romanandreg@gmail.com>.
2
+ Copyright (c) 2009 Noomii inc. <http://www.noomii.com>
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,265 @@
1
+ = Walruz: Simple but Powerful Authorization Framework
2
+
3
+ == Basic and Terminology
4
+
5
+ Walruz facilitates the separation between the authorization process on the business logic and the actions executed after the validation of the authorizations. To understand how it works, we will follow the following terminology:
6
+
7
+ - Subject: Object that is going to be managed (Profile, Posts)
8
+ - Actor: Entity that wants to perform an action on a subject (User, Admin)
9
+ - Policy: A set of rules that tells if the Actor can perform the desired action on the Subject
10
+
11
+ == Walruz Architecture
12
+
13
+ Walruz provides modules and classes that helps on the implementation of the concepts given previously, this are:
14
+
15
+ - Walruz::Subject
16
+ Module that provides the interface to associate policies to an action in the subject
17
+
18
+ - Walruz::Actor
19
+ Module that provides the interface to perform queries to validate if an action can be done between the
20
+ actor and the subject
21
+
22
+ - Walruz::Policy
23
+ Class that provides the interface to implement authorization logic
24
+
25
+ == Subjects specify which policies are related to which actions
26
+
27
+ Subject classes may specify a set of actions that can be performed to them using the check_authorization method
28
+
29
+ class ASubject
30
+ include Walruz::Subject
31
+
32
+ check_authorization :show => Policy1,
33
+ :edit => Policy2
34
+ end
35
+
36
+ If there is just one policy to every possible action performed to the subject, you may specify the :default action, or just specify the Policy class.
37
+
38
+ Example:
39
+
40
+ class ASubject
41
+ include Walruz::Subject
42
+
43
+ check_authorization PolicyClazz
44
+ end
45
+
46
+ or
47
+
48
+ class AnotherSubject
49
+ include Walruz::Subject
50
+
51
+ check_authorization :default => PolicyClazz
52
+ end
53
+
54
+ You can also specify other flags with the default flag.
55
+
56
+ class ASubject
57
+ include Walruz::Subject
58
+
59
+ check_authorization :show => Policy1,
60
+ :edit => Policy2,
61
+ :default => DefaultPolicy
62
+ end
63
+
64
+ == Actors verify if they are able to perform an action on a subject
65
+
66
+ Actor classes can use several methods to check if the actor instance can perform the given action on the subject. This are:
67
+
68
+ - `can?(action, subject)`
69
+ Returns boolean that says if the actor can execute or not the action on the subject, if a block is given, this will be executed when the authorization is true, and a parameters hash from the policy will be passed to the block.
70
+
71
+ - `can!(action, subject)`
72
+ In case the actor can execute the action on the subject, it returns the parameters hash from the policy, otherwise it will raise a Walruz::NotAuthorized.
73
+
74
+ - `satisfies?(policy_class, subject)`
75
+ It behaves just like the `can?` method, but instead of giving an action to be executed to the subject, it gives a policy
76
+
77
+ In case the given action is not assigned to any policy, a default Policy will be executed (if given), if no default policy is given then a Walruz::FlagNotFound exception will be raised.
78
+
79
+ Examples:
80
+
81
+ current_user.can?(:read, friends_profile) do |policy_params|
82
+ # code to be executed if current user can read a friends profile
83
+ end
84
+
85
+ current_user.satisfies?(ActorIsAdmin, nil) do
86
+ # execute some admin logic
87
+ end
88
+
89
+ policy_params = current_user.can!(:read, friends_profile)
90
+ # all the code executed bellow will be executed when the actor is authorized
91
+
92
+ == Implementing Policies
93
+
94
+ To implement a Policy, its necessary that the Policy class inherits from the Walruz::Policy class. This class provides a method called `authorized?` that return either a Boolean, or an Array of two items, the first one being a Boolean, and the second being a Hash of parameters returned from the Policy.
95
+
96
+ Examples:
97
+
98
+ class ActorIsAdmin < Walruz::Policy
99
+
100
+ def authorized?(actor, _)
101
+ actor.is_admin?
102
+ end
103
+
104
+ end
105
+
106
+ class UserIsFriend < Walruz::Policy
107
+
108
+ def authorized?(current_user, friend)
109
+ friendship = Friendship.first(:conditions => { :friend_id => current_user.id, :owner_id => friend.id})
110
+ if !friendship.nil?
111
+ [true, {
112
+ :friendship => friendship
113
+ }]
114
+ else
115
+ false
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ == Composing basic policies to create complex ones
122
+
123
+ Sometimes policies can turn really messy, specially when you have a complex business model. The good news is that, normally this complex policies are a composition of more simple policies (eg. ActorCanSeeUserPictures). Instead of creating this new classes that replicates the same logic of basic policies, we could merge them together in the following way:
124
+
125
+ ActorCanSeeUserPictures = Walruz::Utils.andP(UserIsFriend, UserAllowsDisclosureOfPictures)
126
+
127
+ There is also the utility methods `orP` and `notP`, to create combinations of policies.
128
+
129
+ If your policy returns a parameters hash, and you are using the `andP` method, the parameters will be merged on each policy invocation, if you are using the `orP` method, the parameters of the first policy that returns true will be returned.
130
+
131
+ One other thing that the utility methods do for you is leave its track on the returned policy parameters, when you invoke a composite policy, every policy will leave a symbol with the name of the policy underscored and a question mark at the end, that way you can know which policies were successful or not.
132
+
133
+ Example:
134
+
135
+ class ActorIsAdmin < Walruz::Policy
136
+ # code from above here ...
137
+ end
138
+
139
+ class ActorIsSubject < Walruz::Policy
140
+ def authorized?(actor, subject); actor == subject; end
141
+ end
142
+
143
+ UserReadPolicy = orP(ActorIsSubject, ActorIsAdmin)
144
+
145
+ class User < AbstractORM
146
+ include Walruz::Subject
147
+
148
+ check_authorizations :read => UserReadPolicy
149
+ end
150
+
151
+ current_user.can?(:read, other_user) do |policy_params|
152
+ if policy_params[:actor_is_subject?]
153
+ # do logic of the user interacting with herself
154
+ elsif policy_params[:actor_is_admin?]
155
+ # do logic of the admin user interacting with other user
156
+ else
157
+ # do other logic here...
158
+ end
159
+ end
160
+
161
+ == Dependencies between Policies
162
+
163
+ Sometimes you would like to have a Policy that strictly depends in other ones, on the previous example `UserAllowsDisclosureOfPictures` could have a dependency that says that only the User allows the disclosure of pictures if and only if there is a friend relationship, so we could re-implement this policy as:
164
+
165
+ Example:
166
+
167
+ class UserAllowsDisclosureOfPictures < Walruz::Policy
168
+ depends_on UserIsFriend
169
+ # ...
170
+ end
171
+
172
+ Suppose you need the parameters returned by the previous Policy, you can have them with the params method, it works just like a request.params from any Web Framework in Ruby.
173
+
174
+ Example:
175
+
176
+ class UserAllowsDisclosureOfPictures < Walruz::Policy
177
+ depends_on UserIsFriend
178
+
179
+ def authorized?(_, _)
180
+ params[:friendship].allows_disclosure_of_images?
181
+ end
182
+
183
+ end
184
+
185
+ == Policy combinators, Lifting to the rescue!
186
+
187
+ Sometimes you would like to execute policies that are not directly related to a subject, but to the association of a subject. Given the example above of the friendship relationship and the disclosure of pictures, sometimes you would like to check if a user can see a picture directly on the picture model.
188
+
189
+ Suppose we have the following model in our system:
190
+
191
+ class Picture < AbstractORM
192
+ belongs_to :owner
193
+ end
194
+
195
+ and we would like to check if the current_user can see (read) the picture using:
196
+
197
+ current_user.can(:read, picture_instance)
198
+
199
+ If you may recall, we already implemented the logic that checks that authorization in UserAllowsDisclosureOfPictures, but that policy only works when the subject is of class User; given that you have a subject of class Picture you can not re-use this policy.
200
+
201
+ You could solve this issue doing the following:
202
+
203
+ class PictureReadPolicy < Walruz::Policy
204
+
205
+ def authorized?(user, image)
206
+ user.satisfies?(UserAllowsDisclosureOfPictures, image.owner)
207
+ end
208
+
209
+ end
210
+
211
+ But as you may see, we are just creating new policies to handle old ones, we are not combining the policies effectively. To avoid this caveat, you can use the `lift_subject` method:
212
+
213
+ PictureReadPolicy = lift_subject(:owner, UserAllowsDisclosureOfPictures)
214
+
215
+ class Picture < AbstractORM
216
+ include Walruz::Subject
217
+ belongs_to :owner
218
+
219
+ check_authorizations :read => PictureReadPolicy
220
+ end
221
+
222
+ The first parameter of `lift_subject` is the name of the method that will return a new subject, this new subject is then passed through the policy specified on the second parameter and then executes the Policy checking. Pretty neat eh?
223
+
224
+ == Returning custom errors
225
+
226
+ Suppose you want to add an error to the authorization failure that is a more descriptive, you can do so on the `authorized?` method passing a hash with a :error_message key on the false return. If you use the `can!` method on the actor model, this will become the `Walruz::NotAuthorized` error message.
227
+
228
+ Example:
229
+
230
+ class SomePolicy < Walruz::Policy
231
+
232
+ def authorized?(actor, subject)
233
+ # some complex logic here
234
+ return [false, {
235
+ :error_message => 'More descriptive error message'
236
+ }]
237
+ end
238
+ end
239
+
240
+
241
+ == Conventions
242
+
243
+ You'll notice that once you start implementing policies for your system, you'll be lost soon enough asking yourself which type of subject a Policy receives; to avoid such confusions, we suggest that you apply the following rules:
244
+
245
+ - The first name of the policy should be the Subject class (e.g. UserIsFriend)
246
+ - If the policy only applies to the actor, the policy class name should start with the Actor word (e.g. ActorIsAdmin)
247
+ - You should always have the compositions of policies in just one place in your library folder (e.g. in policies.rb file).
248
+ - The result of policy compositions should finish with the word Policy (e.g `UserDeletePolicy = orP(ActorIsSubject, ActorIsAdmin`))
249
+ - Use `lift_subject` when you are combining the lifted policy with other policies, if you are not doing this, consider checking authorizations on parents of the subject instead of the subject (e.g. current_user.can?(:see_pictures_of, picture.owner))
250
+
251
+ If you follow this rules, it will be much easier for you to merge policies together in an efficient way.
252
+
253
+ == Rails Integration
254
+
255
+ See the "walruz-rails":http://github.com/noomii/walruz-rails gem
256
+
257
+ == More examples
258
+
259
+ You may check the project in the examples/ directory for more info; on the rails project, take a look on the spec/models/beatle_spec.rb file, it's really illustrating.
260
+
261
+ == Copyright
262
+
263
+ Copyright (c) 2009 Roman Gonzalez <romanandreg@gmail.com>.
264
+ Copyright (c) 2009 Noomii inc. <http://www.noomii.com>
265
+ All rights reserved.
data/Rakefile ADDED
@@ -0,0 +1,75 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "walruz"
8
+ gem.summary = %Q{Walruz is a gem that provides an easy but powerful way to implement authorization policies in a system, relying on the composition of simple policies to create more complex ones.}
9
+ gem.email = "roman@noomi.com"
10
+ gem.homepage = "http://github.com/roman/walruz"
11
+ gem.authors = ["Roman Gonzalez"]
12
+ gem.rubyforge_project = "walruz"
13
+
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
18
+ end
19
+
20
+ require 'spec/rake/spectask'
21
+ Spec::Rake::SpecTask.new(:spec) do |spec|
22
+ spec.libs << 'lib' << 'spec'
23
+ spec.spec_files = FileList['spec/**/*_spec.rb']
24
+ spec.spec_opts = ['--options', "\"%s/spec/spec.opts\"" % File.dirname(__FILE__)]
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+
34
+ task :default => :spec
35
+
36
+ require 'rake/rdoctask'
37
+ Rake::RDocTask.new do |rdoc|
38
+ if File.exist?('VERSION.yml')
39
+ config = YAML.load(File.read('VERSION.yml'))
40
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
41
+ else
42
+ version = ""
43
+ end
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "walruz #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
50
+
51
+ begin
52
+ require 'rake/contrib/sshpublisher'
53
+ namespace :rubyforge do
54
+
55
+ desc "Release gem and RDoc documentation to RubyForge"
56
+ task :release => ["rubyforge:release:gem", "rubyforge:release:docs"]
57
+
58
+ namespace :release do
59
+ desc "Publish RDoc to RubyForge."
60
+ task :docs => [:rdoc] do
61
+ config = YAML.load(
62
+ File.read(File.expand_path('~/.rubyforge/user-config.yml'))
63
+ )
64
+
65
+ host = "#{config['username']}@rubyforge.org"
66
+ remote_dir = "/var/www/gforge-projects/walruz/"
67
+ local_dir = 'rdoc'
68
+
69
+ Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
70
+ end
71
+ end
72
+ end
73
+ rescue LoadError
74
+ puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
75
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 3
@@ -0,0 +1,104 @@
1
+ module Walruz
2
+
3
+ #
4
+ # Actors have the role to use subjects, so they are the ones
5
+ # who can or cannot do something with a given subject
6
+ #
7
+ module Actor
8
+
9
+ #
10
+ # Allows an actor to check if he can do some action on a given
11
+ # subject. It is normally used with a block that get's executed if the
12
+ # actor can execute the given action on the subject.
13
+ #
14
+ # Params:
15
+ # - label: The label of the action
16
+ # - subject: The subject which the actor wants to interact with
17
+ #
18
+ # Returns:
19
+ # It returns a boolean indicating that the actor is authorized to
20
+ # access (or not) the subject
21
+ #
22
+ # Notes:
23
+ # Because this method is probably going to be called multiple times on
24
+ # a same action, the result of the first invocation is cached, if you
25
+ # want to uncache just pass true as a third parameter.
26
+ #
27
+ #
28
+ def can?(*args)
29
+ if args.size == 2
30
+ cached_values_for_can[args] ||= can_without_caching?(*args)
31
+ elsif args.size == 3
32
+ if args.pop
33
+ cached_values_for_can[args] = can_without_caching?(*args)
34
+ else
35
+ cached_values_for_can[args] ||= can_without_caching?(*args)
36
+ end
37
+ else
38
+ raise ArgumentError.new("wrong number of arguments (%d for 2)" % args.size)
39
+ end
40
+ end
41
+
42
+ def can_without_caching?(label, subject)
43
+ subject.can_be?(label, self)[0]
44
+ end
45
+
46
+ def cached_values_for_can
47
+ @_cached_values_for_can ||= {}
48
+ end
49
+
50
+ #
51
+ # Allows an actor to check if he can do some action on a given
52
+ # subject.
53
+ #
54
+ # Params:
55
+ # - label: The label of the action
56
+ # - subject: The subject which the actor wants to interact with
57
+ #
58
+ # Returns:
59
+ # It can return either a Boolean or an Array of the form [Boolean, Hash].
60
+ # When is an Array, the second parameter is a Hash with parameters given from
61
+ # the policy.
62
+ #
63
+ # Raises:
64
+ # Walruz::NotAuthorized error if the actor can't interact with the subject
65
+ #
66
+ def can!(label, subject)
67
+ result = subject.can_be?(label, self)
68
+ if result[0]
69
+ cached_values_for_can[[label, subject]] = result[0]
70
+ result[1]
71
+ else
72
+ response_params = result[1]
73
+ error_message = response_params[:error_message] || "You are not authorized to access this content"
74
+ raise NotAuthorized.new(error_message)
75
+ end
76
+ end
77
+
78
+ #
79
+ # Allows an actor to check if he the given policy applies to him and the given subject.
80
+ #
81
+ # Params:
82
+ # - policy: label of the Policy the actor wants to check
83
+ # - subject: The subject which the actor wants to interact with
84
+ #
85
+ # block |policy_hash|:
86
+ # If the actor can access the subject, then the block will be executed;
87
+ # this will receive the policy hash as a parameter.
88
+ #
89
+ # Returns:
90
+ # It returns a boolean indicating that the actor is authorized to
91
+ # access (or not) the subject with the given Policy.
92
+ #
93
+ def satisfies?(policy_label, subject, &block)
94
+ policy_clz = Walruz.policies[policy_label]
95
+ raise ActionNotFound.new(:policy_label, :label => policy_label) if policy_clz.nil?
96
+ result = policy_clz.return_policy.new.safe_authorized?(self, subject)
97
+ if result[0]
98
+ block.call(result[1]) if block_given?
99
+ end
100
+ result[0]
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,160 @@
1
+ module Walruz
2
+
3
+ #
4
+ # One of the cores of the framework, it's purpuse is to encapsulate
5
+ # some authorization logic with a given actor and subject.
6
+ # It's main method `authorized?(actor, subject)` verifies that
7
+ # an actor is actually authorized to manage the subject.
8
+ #
9
+ class Policy
10
+ extend Walruz::Utils
11
+
12
+ attr_reader :params
13
+
14
+ # @private
15
+ def self.inherited(child)
16
+ @policies ||= {}
17
+ unless child.policy_label.nil?
18
+ @policies[child.policy_label] = child
19
+ end
20
+ end
21
+
22
+ #
23
+ # See Walruz.policies
24
+ #
25
+ def self.policies
26
+ @policies || {}
27
+ end
28
+
29
+ #
30
+ # Returns a Proc with a curried actor, making it easier
31
+ # to perform validations of a policy in an Array of subjects
32
+ # Params:
33
+ # - actor: The actor who checks if it is authorized
34
+ #
35
+ # Returns: (subject -> [Bool, Hash])
36
+ #
37
+ # Example:
38
+ # subjects.filter(&PolicyXYZ.with_actor(some_user))
39
+ #
40
+ def self.with_actor(actor)
41
+ policy_instance = self.new
42
+ lambda do |subject|
43
+ policy_instance.safe_authorized?(actor, subject)[0]
44
+ end
45
+ end
46
+
47
+ #
48
+ # Stablish other Policy dependencies, so that they are executed
49
+ # before the current one, giving chances to receive the previous
50
+ # policies return parameters
51
+ #
52
+ # Example:
53
+ # class FriendEditProfilePolicy
54
+ # depends_on FriendPolicy
55
+ #
56
+ # def authorized?(actor, subject)
57
+ # params[:friend_relationship].can_edit? # for friend metadata
58
+ # end
59
+ #
60
+ # end
61
+ #
62
+ def self.depends_on(*other_policies)
63
+ self.policy_dependencies = (other_policies << self)
64
+ end
65
+
66
+ # Utility for depends_on macro
67
+ # @private
68
+ def self.policy_dependencies=(dependencies)
69
+ @_policy_dependencies = dependencies
70
+ end
71
+
72
+ # Utility for depends_on macro
73
+ # @private
74
+ def self.policy_dependencies
75
+ @_policy_dependencies
76
+ end
77
+
78
+ # Utility for depends_on macro
79
+ # @private
80
+ def self.return_policy
81
+ if policy_dependencies.nil?
82
+ self
83
+ else
84
+ andP(*policy_dependencies)
85
+ end
86
+ end
87
+
88
+ # Utility method (copied from ActiveSupport)
89
+ # @private
90
+ def self.underscore(camel_cased_word)
91
+ if camel_cased_word.empty?
92
+ camel_cased_word
93
+ else
94
+ camel_cased_word.to_s.split('::').last.
95
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
96
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
97
+ tr("-", "_").
98
+ downcase
99
+ end
100
+ end
101
+
102
+
103
+ #
104
+ # Verifies if the actor is authorized to interact with the subject
105
+ # Params:
106
+ # - actor: The object who checks if it is authorized
107
+ # - subject: The object that is going to be accesed
108
+ #
109
+ # Returns: [Bool, Hash]
110
+ #
111
+ def authorized?(actor, subject)
112
+ raise NotImplementedError.new("You need to implement policy")
113
+ end
114
+
115
+ #
116
+ # Returns the identifier of the Policy that will be setted on the
117
+ # policy params hash once the authorization is executed.
118
+ #
119
+ # Returns:
120
+ # By default it will return a symbol with the name of the Policy class in underscore (unless the policy label
121
+ # was setted, in that case the policy label will be used) with an '?' appended
122
+ #
123
+ def self.policy_keyword
124
+ if self.policy_label.nil?
125
+ nil
126
+ else
127
+ :"#{self.policy_label}?"
128
+ end
129
+
130
+ end
131
+
132
+ def self.policy_label
133
+ @policy_label ||= (self.name.empty? ? nil : :"#{self.underscore(self.name)}")
134
+ end
135
+
136
+ #
137
+ # Sets the identifier of the Policy for using on the `satisfies?` method
138
+ #
139
+ # Parameters:
140
+ # - label: Symbol that represents the policy
141
+ #
142
+ def self.set_policy_label(label)
143
+ @policy_label = label
144
+ end
145
+
146
+ # @private
147
+ def safe_authorized?(actor, subject)
148
+ result = Array(authorized?(actor, subject))
149
+ result[1] ||= {}
150
+ result[1][self.class.policy_keyword] = result[0] unless self.class.policy_keyword.nil?
151
+ result
152
+ end
153
+
154
+ # @private
155
+ def set_params(params)
156
+ @params = params
157
+ end
158
+
159
+ end
160
+ end