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 +2 -8
- data/README.md +275 -0
- data/Rakefile +4 -25
- data/consul.gemspec +20 -96
- data/lib/consul.rb +1 -0
- data/lib/consul/active_record.rb +14 -0
- data/lib/consul/controller.rb +29 -0
- data/lib/consul/errors.rb +4 -3
- data/lib/consul/power.rb +2 -0
- data/lib/consul/version.rb +3 -0
- data/spec/app_root/app/controllers/application_controller.rb +2 -4
- data/spec/app_root/app/controllers/dashboards_controller.rb +12 -1
- data/spec/app_root/app/controllers/songs_controller.rb +3 -4
- data/spec/app_root/app/controllers/users_controller.rb +3 -3
- data/spec/app_root/app/models/power.rb +10 -6
- data/spec/app_root/config/routes.rb +1 -1
- data/spec/app_root/db/migrate/001_create_users.rb +3 -1
- data/spec/consul/active_record_spec.rb +37 -0
- data/spec/consul/power_spec.rb +17 -6
- data/spec/controllers/dashboards_controller_spec.rb +17 -0
- data/spec/controllers/songs_controller_spec.rb +4 -4
- data/spec/controllers/users_controller_spec.rb +3 -3
- data/spec/spec_helper.rb +10 -1
- data/spec/support/spec.opts +4 -0
- data/spec/support/spec_candy.rb +179 -0
- metadata +124 -42
- data/README.rdoc +0 -213
- data/VERSION +0 -1
data/Gemfile
CHANGED
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
|
-
|
2
|
-
|
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 =
|
8
|
-
s.version =
|
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.
|
13
|
-
s.
|
14
|
-
s.
|
15
|
-
s.
|
16
|
-
|
17
|
-
|
18
|
-
s.files
|
19
|
-
|
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
|
-
|
95
|
-
|
18
|
+
s.add_dependency('rails')
|
19
|
+
s.add_dependency('assignable_values')
|
96
20
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|