consul 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of consul might be problematic. Click here for more details.

data/Gemfile CHANGED
@@ -1,9 +1,3 @@
1
- source 'http://rubygems.org'
1
+ source :rubygems
2
2
 
3
- gem 'rails', '=2.3.11'
4
- gem 'consul', :path => '.'
5
- gem 'rspec', '=1.3.1'
6
- gem 'rspec-rails', '=1.3.3'
7
- gem 'jeweler'
8
- gem 'ruby-debug'
9
- gem 'sqlite3-ruby'
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,275 @@
1
+ Consul - A scope-based authorization solution
2
+ =============================================
3
+
4
+ Consul is a authorization solution for Ruby on Rails that uses scopes to control what a user can see or edit.
5
+
6
+ We have used Consul in combination with [assignable_values](https://github.com/makandra/assignable_values) to solve a variety of authorization requirements ranging from boring to bizarre.
7
+
8
+
9
+ Describing a power for your application
10
+ ---------------------------------------
11
+
12
+ You describe access to your application by putting a `Power` model into `app/models/power.rb`:
13
+
14
+ class Power
15
+ include Consul::Power
16
+
17
+ def initialize(user)
18
+ @user = user
19
+ end
20
+
21
+ power :notes do
22
+ Note.by_author(@user)
23
+ end
24
+
25
+ power :users do
26
+ User if @user.admin?
27
+ end
28
+
29
+ power :dashboard do
30
+ true # not a scope, but a boolean power. This is useful to control access to stuff that doesn't live in the database.
31
+ end
32
+
33
+ end
34
+
35
+ There are no restrictions on the name or constructor arguments of your power class.
36
+
37
+
38
+ Querying a power
39
+ ----------------
40
+
41
+ Common things you might want from a power:
42
+
43
+ 1. Get its scope
44
+ 2. Ask whether it is there
45
+ 3. Raise an error unless it its there
46
+ 4. Ask whether a given record is included in its scope
47
+ 5. Raise an error unless a given record is included in its scope
48
+
49
+ Here is how to do all of that:
50
+
51
+ power = Power.new(user)
52
+ power.notes # => returns an ActiveRecord::Scope
53
+ power.notes? # => returns true if Power#notes returns a scope
54
+ power.notes! # => raises Consul::Powerless unless Power#notes returns a scope
55
+ power.note?(Note.last) # => returns whether the given Note is in the Power#notes scope. Caches the result for subsequent queries.
56
+ power.note!(Note.last) # => raises Consul::Powerless unless the given Note is in the Power#notes scope
57
+
58
+ You can also write power checks like this:
59
+
60
+ power.include?(:notes)
61
+ power.include!(:notes)
62
+ power.include?(:note, Note.last)
63
+ power.include!(:note, Note.last)
64
+
65
+
66
+ Boolean powers
67
+ --------------
68
+
69
+ Boolean powers are useful to control access to stuff that doesn't live in the database:
70
+
71
+ class Power
72
+ ...
73
+
74
+ power :dashboard do
75
+ true
76
+ end
77
+
78
+ end
79
+
80
+ You can query it like the other powers:
81
+
82
+ power.dashboard? # => true
83
+ power.dashboard! # => raises Consul::Powerless unless Power#dashboard? returns true
84
+
85
+
86
+ Role-based permissions
87
+ ----------------------
88
+
89
+ Consul has no built-in support for role-based permissions, but you can easily implement it yourself. Let's say your `User` model has a string column `role` which can be `"author"` or `"admin"`:
90
+
91
+ class Power
92
+ include Consul::Power
93
+
94
+ def initialize(user)
95
+ @user = user
96
+ end
97
+
98
+ power :notes do
99
+ case role
100
+ when :admin then Note
101
+ when :author then Note.by_author
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def role
108
+ @user.role.to_sym
109
+ end
110
+
111
+ end
112
+
113
+
114
+ Controller integration
115
+ ----------------------
116
+
117
+ It is convenient to expose the power for the current request to the rest of the application. Consul will help you with that if you tell it how to instantiate a power for the current request:
118
+
119
+ class ApplicationController < ActionController::Base
120
+ include Consul::Controller
121
+
122
+ current_power do
123
+ Power.new(current_user)
124
+ end
125
+
126
+ end
127
+
128
+ You now have a helper method `current_power` for your controller and views. Everywhere else, you can access it from `Power.current`. The power will be instantiated when the request is handed over from routing to `ApplicationController`, and will be nilified once the request was processed.
129
+
130
+ You can now use power scopes to control access:
131
+
132
+ class NotesController < ApplicationController
133
+
134
+ def show
135
+ @note = current_power.notes.find(params[:id])
136
+ end
137
+
138
+ end
139
+
140
+ To make sure a power is given before every action in a controller:
141
+
142
+ class NotesController < ApplicationController
143
+ power :notes
144
+ end
145
+
146
+ You can use `:except` and `:only` options like in before filters.
147
+
148
+ You can also map different powers to different actions:
149
+
150
+ class NotesController < ApplicationController
151
+ power :notes, :map => { [:edit, :update, :destroy] => :changable_notes }
152
+ end
153
+
154
+ It is often convenient to map a power scope to a private controller method:
155
+
156
+ class NotesController < ApplicationController
157
+
158
+ power :notes, :as => end_of_association_chain
159
+
160
+ def show
161
+ @note = end_of_association_chain.find(params[:id])
162
+ end
163
+
164
+ end
165
+
166
+ This is especially useful when you are using a RESTful controller library like [resource_controller](https://github.com/jamesgolick/resource_controller). The mapped method is aware of the `:map` option.
167
+
168
+ You can force yourself to use a `power` check in every controller. This will raise `Consul::UncheckedPower` if you ever forget it:
169
+
170
+ class ApplicationController < ActionController::Base
171
+ include Consul::Controller
172
+ require_power_check
173
+ end
174
+
175
+ Should you for some obscure reason want to forego the power check:
176
+
177
+ class ApiController < ApplicationController
178
+ skip_power_check
179
+ end
180
+
181
+
182
+ Validating assignable values
183
+ ----------------------------
184
+
185
+ Sometimes a scope is not enough to express what a user can edit. You will often want to give a user write access to a record, but restrict the values she can assign to a given field.
186
+
187
+ Consul leverages the [assignable_values](https://github.com/makandra/assignable_values) gem to add an optional authorization layer to your models. This layer adds additional validations in the context of a request, but skips those validations in other contexts (console, background jobs, etc.).
188
+
189
+ You can enable the authorization layer by using the macro `authorize_values_for`:
190
+
191
+ class Story < ActiveRecord::Base
192
+ authorize_values_for :state
193
+ endy
194
+
195
+ The macro defines an accessor `power` on instances of `Story`. If that field is set to a power, the values of `state` will be validated against a whitelist of values provided by that power. If that field is `nil`, the validation is skipped.
196
+
197
+ Here is a power implementation that can provide a list of assignable values for the example above:
198
+
199
+ class Power
200
+ ...
201
+
202
+ def assignable_story_states(story)
203
+ if admin?
204
+ ['delivered', 'accepted', 'rejected']
205
+ else
206
+ ['delivered']
207
+ end
208
+ end
209
+
210
+ end
211
+
212
+ Here you can see how to activate the authorization layer and use the new validations:
213
+
214
+ story = Story.new
215
+ story.power = Power.current # activate the authorization layer
216
+
217
+ story.assignable_states # ['delivered'] # apparently we're not admins
218
+
219
+ story.state = 'accepted' # a disallowed value
220
+ story.valid? # => false
221
+
222
+ story.state = 'delivered' # an allowed value
223
+ story.valid? # => true
224
+
225
+ You can not only authorize scalar attributes like strings or integers that way, you can also authorize `belongs_to` associations:
226
+
227
+ class Story < ActiveRecord::Base
228
+ belongs_to :project
229
+ authorize_values_for :project
230
+ end
231
+
232
+ class Power
233
+ ...
234
+
235
+ def assignable_story_projects(story)
236
+ user.account.projects
237
+ end
238
+ end
239
+
240
+ The `authorize_values_for` macro comes with many useful options and details best explained in the [assignable_values README](https://github.com/makandra/assignable_values), so head over there for more. The macro is basically a shortcut for this:
241
+
242
+ attr_accessor :power
243
+ assignable_values_for :field, :through => lambda { Power.current }
244
+
245
+
246
+ Installation
247
+ ------------
248
+
249
+ Add the following to your `Gemfile`:
250
+
251
+ gem 'consul'
252
+
253
+ Now run `bundle install` to lock the gem into your project.
254
+
255
+
256
+ Development
257
+ -----------
258
+
259
+ A Rails 2 test application lives in `spec/app_root`. You can run specs from the project root by saying:
260
+
261
+ bundle exec rake spec
262
+
263
+ If you would like to contribute:
264
+
265
+ - Fork the repository.
266
+ - Push your changes **with specs**.
267
+ - Send me a pull request.
268
+
269
+ I'm very eager to keep this gem leightweight and on topic. If you're unsure whether a change would make it into the gem, [talk to me beforehand](henning.koch@makandra.de).
270
+
271
+
272
+ Credits
273
+ -------
274
+
275
+ Henning Koch from [makandra](http://makandra.com/)
data/Rakefile CHANGED
@@ -1,33 +1,12 @@
1
1
  require 'rake'
2
- require 'rake/rdoctask'
3
2
  require 'spec/rake/spectask'
3
+ require 'bundler/gem_tasks'
4
4
 
5
+ desc 'Default: Run all specs.'
5
6
  task :default => :spec
6
7
 
8
+ desc "Run all specs"
7
9
  Spec::Rake::SpecTask.new() do |t|
8
- t.spec_opts = ['--options', "\"spec/spec.opts\""]
10
+ t.spec_opts = ['--options', "\"spec/support/spec.opts\""]
9
11
  t.spec_files = FileList['spec/**/*_spec.rb']
10
12
  end
11
-
12
- desc 'Generate documentation for the consul gem'
13
- Rake::RDocTask.new(:rdoc) do |rdoc|
14
- rdoc.rdoc_dir = 'rdoc'
15
- rdoc.title = 'consul'
16
- rdoc.options << '--line-numbers' << '--inline-source'
17
- rdoc.rdoc_files.include('README')
18
- rdoc.rdoc_files.include('lib/**/*.rb')
19
- end
20
-
21
- begin
22
- require 'jeweler'
23
- Jeweler::Tasks.new do |gemspec|
24
- gemspec.name = "consul"
25
- gemspec.summary = "Scope-based authorization solution for Rails"
26
- gemspec.email = "henning.koch@makandra.de"
27
- gemspec.homepage = "http://github.com/makandra/consul"
28
- gemspec.description = "Consul is a scope-based authorization solution for Ruby on Rails."
29
- gemspec.authors = ["Henning Koch"]
30
- end
31
- rescue LoadError
32
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
33
- end
data/consul.gemspec CHANGED
@@ -1,103 +1,27 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
- # -*- encoding: utf-8 -*-
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "consul/version"
5
3
 
6
4
  Gem::Specification.new do |s|
7
- s.name = %q{consul}
8
- s.version = "0.1.2"
9
-
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
5
+ s.name = 'consul'
6
+ s.version = Consul::VERSION
11
7
  s.authors = ["Henning Koch"]
12
- s.date = %q{2011-07-07}
13
- s.description = %q{Consul is a scope-based authorization solution for Ruby on Rails.}
14
- s.email = %q{henning.koch@makandra.de}
15
- s.extra_rdoc_files = [
16
- "README.rdoc"
17
- ]
18
- s.files = [
19
- ".gitignore",
20
- "Gemfile",
21
- "README.rdoc",
22
- "Rakefile",
23
- "VERSION",
24
- "consul.gemspec",
25
- "lib/consul.rb",
26
- "lib/consul/controller.rb",
27
- "lib/consul/errors.rb",
28
- "lib/consul/power.rb",
29
- "lib/consul/spec/matchers.rb",
30
- "spec/app_root/app/controllers/application_controller.rb",
31
- "spec/app_root/app/controllers/dashboards_controller.rb",
32
- "spec/app_root/app/controllers/songs_controller.rb",
33
- "spec/app_root/app/controllers/users_controller.rb",
34
- "spec/app_root/app/models/client.rb",
35
- "spec/app_root/app/models/note.rb",
36
- "spec/app_root/app/models/power.rb",
37
- "spec/app_root/app/models/user.rb",
38
- "spec/app_root/config/boot.rb",
39
- "spec/app_root/config/database.yml",
40
- "spec/app_root/config/environment.rb",
41
- "spec/app_root/config/environments/in_memory.rb",
42
- "spec/app_root/config/environments/mysql.rb",
43
- "spec/app_root/config/environments/postgresql.rb",
44
- "spec/app_root/config/environments/sqlite.rb",
45
- "spec/app_root/config/environments/sqlite3.rb",
46
- "spec/app_root/config/routes.rb",
47
- "spec/app_root/db/migrate/001_create_users.rb",
48
- "spec/app_root/db/migrate/002_create_clients.rb",
49
- "spec/app_root/db/migrate/003_create_notes.rb",
50
- "spec/app_root/lib/console_with_fixtures.rb",
51
- "spec/app_root/log/.gitignore",
52
- "spec/app_root/script/console",
53
- "spec/consul/power_spec.rb",
54
- "spec/controllers/dashboards_controller_spec.rb",
55
- "spec/controllers/songs_controller_spec.rb",
56
- "spec/controllers/users_controller_spec.rb",
57
- "spec/rcov.opts",
58
- "spec/spec.opts",
59
- "spec/spec_helper.rb"
60
- ]
61
- s.homepage = %q{http://github.com/makandra/consul}
62
- s.rdoc_options = ["--charset=UTF-8"]
8
+ s.email = 'henning.koch@makandra.de'
9
+ s.homepage = 'https://github.com/makandra/consul'
10
+ s.summary = 'A scope-based authorization solution for Ruby on Rails.'
11
+ s.description = s.summary
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
63
16
  s.require_paths = ["lib"]
64
- s.rubygems_version = %q{1.7.2}
65
- s.summary = %q{Scope-based authorization solution for Rails}
66
- s.test_files = [
67
- "spec/controllers/users_controller_spec.rb",
68
- "spec/controllers/songs_controller_spec.rb",
69
- "spec/controllers/dashboards_controller_spec.rb",
70
- "spec/spec_helper.rb",
71
- "spec/consul/power_spec.rb",
72
- "spec/app_root/app/controllers/songs_controller.rb",
73
- "spec/app_root/app/controllers/dashboards_controller.rb",
74
- "spec/app_root/app/controllers/application_controller.rb",
75
- "spec/app_root/app/controllers/users_controller.rb",
76
- "spec/app_root/app/models/client.rb",
77
- "spec/app_root/app/models/power.rb",
78
- "spec/app_root/app/models/user.rb",
79
- "spec/app_root/app/models/note.rb",
80
- "spec/app_root/lib/console_with_fixtures.rb",
81
- "spec/app_root/config/boot.rb",
82
- "spec/app_root/config/routes.rb",
83
- "spec/app_root/config/environments/mysql.rb",
84
- "spec/app_root/config/environments/sqlite3.rb",
85
- "spec/app_root/config/environments/in_memory.rb",
86
- "spec/app_root/config/environments/postgresql.rb",
87
- "spec/app_root/config/environments/sqlite.rb",
88
- "spec/app_root/config/environment.rb",
89
- "spec/app_root/db/migrate/001_create_users.rb",
90
- "spec/app_root/db/migrate/002_create_clients.rb",
91
- "spec/app_root/db/migrate/003_create_notes.rb"
92
- ]
93
17
 
94
- if s.respond_to? :specification_version then
95
- s.specification_version = 3
18
+ s.add_dependency('rails')
19
+ s.add_dependency('assignable_values')
96
20
 
97
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
98
- else
99
- end
100
- else
101
- end
21
+ # s.add_development_dependency('assignable_values')
22
+ s.add_development_dependency('rails', '~>2.3')
23
+ s.add_development_dependency('rspec', '~>1.3')
24
+ s.add_development_dependency('rspec-rails', '~>1.3')
25
+ s.add_development_dependency('shoulda-matchers')
26
+ s.add_development_dependency('sqlite3')
102
27
  end
103
-