kant 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8aeeefb188062978e189623f81d217b7988ed158
4
+ data.tar.gz: b658a15f162e5a88a2f576394ae12398102084aa
5
+ SHA512:
6
+ metadata.gz: c034e6d3971a058076c5eed08aefad7adc4620d51a09e59dc790c19bcff21cfc0762b70d8fbbc2b8f769722df251714684a9fd3ebcf6f174fc2cbcbe5de5d15d
7
+ data.tar.gz: d4c6c7e75190b3064383d344900f36ba6d8164ba971b2113af64ddde0365272c7b31ac78155c601afb7a88be3d082e9c5ce525eef2c463bf36d1de00d9654003
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kant.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Mark Przepiora
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,406 @@
1
+ # Kant v0.0.2
2
+
3
+ Kant is a tiny authorization library for your Ruby (especially Rails and/or
4
+ ActiveRecord) projects.
5
+
6
+ ## Overview
7
+
8
+ ### What Kant does NOT do:
9
+
10
+ - Add a scope to every single ActiveRecord model in your application.
11
+ - Add any magic methods to your controllers that fetch your data for you, or
12
+ make any assumptions about how you want to do this.
13
+ - Force you to redefine your authorization logic on every single request.
14
+ - Depend on Rails/ActiveRecord--but if you do use these, there's a tiny bit of
15
+ magic available to you if you want to use it.
16
+
17
+ ### What Kant does:
18
+
19
+ - Very little.
20
+ - Allows you to pick and choose how much of it you want to use.
21
+ - Defines a simple interface (two methods) that your `AccessControl` class
22
+ should implement.
23
+ - Provides two simple access control classes (`NoAccess` and `AllAccess`) you
24
+ might want to use for unauthenticated users and admins respectively.
25
+ - For typical use cases, Kant gives you a `PolicyAccess` class which allows you
26
+ to split up your authorization logic into various `FooPolicy` classes, one
27
+ for each of your models. This class uses a minimal amount of magic to work
28
+ with `ActiveRecord` models out of the box, but you can extend it easily to
29
+ use any other ORM.
30
+ - Provides a module you can include in your Rails controllers which gives you
31
+ an interface very similar to the one you might be used to if you are
32
+ currently using CanCanCan.
33
+
34
+ ### Kant's philosophy
35
+
36
+ There are two broad classes of actions you probably need to authorize in your
37
+ application:
38
+
39
+ 1. Single-record access: namely, can a user perform an action on a given
40
+ record? (Yes or No).
41
+ 2. Record index access of some sort: namely, give me a list of the records the
42
+ user is allowed to access.
43
+
44
+ An `AccessControl` class (you might know this as an `Ability` class in CanCan)
45
+ is any plain-old Ruby class that is instantiated with a single argument (your
46
+ user object), and implements two methods: (1) `can?(action, object)`, which
47
+ returns true or false for any action and object (typically a symbol and a
48
+ record), and (2) `accessible(action, scope)` which takes an action and a scope
49
+ of some kind (typically an ActiveRecord scope) and returns a new scope of which
50
+ records the user is allowed to access.
51
+
52
+ Everything else in Kant is either a very simple API on top of this interface to
53
+ make your life easier when using it in a typical Rails app, or an
54
+ implementation of an access control class which might work well in a typical
55
+ Rails app. However, you can use as little or as much of Kant as you want, and
56
+ it certainly doesn't require Rails at all.
57
+
58
+ ## Installation
59
+
60
+ Add this line to your application's Gemfile:
61
+
62
+ ```ruby
63
+ gem 'kant', require: false
64
+ ```
65
+
66
+ And then execute:
67
+
68
+ $ bundle
69
+
70
+ ## Usage
71
+
72
+ ### Controller Mixin
73
+
74
+ Kant provides a `Kant::ControllerMixin` module that you can include in your
75
+ ApplicationController if you wish. It adds a couple of methods:
76
+
77
+ - A `current_access_control` method, which simply returns
78
+ `AccessControl.new(current_user)` (a class which are you are expected to
79
+ provide). You can override this if you need to, for example, choose which
80
+ access control class to use based on the current user's role. See the source
81
+ code for an example.
82
+ - It delegtes `can?(...)` and `accessible(...)` to `current_access_control`.
83
+ - It adds an `authorize!(...)` method, which delegates to `can?(...)`, being a
84
+ no-op if `can?` returns true, but raising a `Kant::AccessDenied` exception if
85
+ `can?` returns false.
86
+
87
+ Be sure to require what you need! Example:
88
+
89
+ ```ruby
90
+ require 'kant/controller_mixin'
91
+
92
+ class ApplicationController < ActionController::Base
93
+ include Kant::ControllerMixin
94
+
95
+ # ...
96
+ end
97
+ ```
98
+
99
+ ### Basic All or Nothing Access Controls
100
+
101
+ If you are using ActiveRecord, Kant provides `Kant::AllAccess` and
102
+ `Kant::NoAccess` which you can use for your admins and unauthenticated users
103
+ respectively.
104
+
105
+ `AllAccess` returns true for every `can?` query, and returns `scope.all` for
106
+ every `accessible` query. In other words, everything is permitted.
107
+
108
+ `NoAccess` returns false for every `can?` query, and returns `scope.none` for
109
+ every `accessible` query, denying all access.
110
+
111
+ ### Policy-based Access Controls
112
+
113
+ While you could implement your own access control class from scratch, you
114
+ probably just have a typical Rails app in which you just want to implement
115
+ per-model, and per-action authorization logic. For this, there's
116
+ `Kant::PolicyAccess`.
117
+
118
+ This is the only part of Kant that contains magic, and assumes you are using
119
+ ActiveRecord, but we'll explain the magic entirely with an example:
120
+
121
+ When a `PolicyAccess` is queried with `can?(:update, foo)`, where `foo` is an
122
+ instance of a class named `Foo`, it will look for a class/module named
123
+ `FooPolicy`, and a class method on it called `can_update?(record, user)`. It
124
+ simply delegates to this function.
125
+
126
+ For example,
127
+
128
+ ```ruby
129
+ module FooPolicy
130
+ def self.can_update?(foo, user)
131
+ foo.owner_id == user.id
132
+ end
133
+ end
134
+ ```
135
+
136
+ If either `FooPolicy` does not exist, or if `can_update?` is undefined, `can?`
137
+ will simply return false.
138
+
139
+ When a `PolicyAccess` is queried with `accessible(:read, Foo)` where `Foo` is
140
+ an ActiveRecord model class (or a scope of that model), Kant will again look
141
+ for `FooPolicy`, this time with an instance method called
142
+ `readable(scope, user)`, which will be called with `readable(Foo, user)`.
143
+
144
+ If either the class or method doesn't exist, `Foo.none` will be returned.
145
+
146
+ For example,
147
+
148
+ ```ruby
149
+ module FooPolicy
150
+ def self.readable(foos, user)
151
+ foos.where(owner_id: user.id)
152
+ end
153
+ end
154
+ ```
155
+
156
+ There is one added bonus to keep in mind. If your policy class implements
157
+ `readable` but not `can_read?`, and if `PolicyAccess` is queried with
158
+ `can?(:read, foo)`, then it will return,
159
+
160
+ ```ruby
161
+ FooPolicy.readable(Foo, user).where(id: foo.id).any?
162
+ ```
163
+
164
+ Therefore, you can typically implement just a scope policy for an action and
165
+ let the single-object policy be generated automatically. You may want to
166
+ implement (in this example) `can_read?(...)` anyway, since you can avoid an
167
+ extra SQL query by simply comparing `foo.owner_id == user.id`. However, in
168
+ real-world apps, authorizing an action often requires executing a query of some
169
+ sort anyway, so it's your choice.
170
+
171
+ Since scope and object policies are just methods, you can alias them. No magic
172
+ required. For example,
173
+
174
+ ```ruby
175
+ module FooPolicy
176
+ def self.can_read?(foo, user)
177
+ foo.owner_id == user.id
178
+ end
179
+
180
+ class << self
181
+ alias_method :can_update?, :can_read?
182
+ alias_method :can_destroy?, :can_read?
183
+ end
184
+ end
185
+ ```
186
+
187
+ ## Okay Practices
188
+
189
+ ### Controller Params
190
+
191
+ This isn't enforced by Kant at all, but you could define your allowed
192
+ controller params inside your policy classes. For example:
193
+
194
+ ```ruby
195
+ module FooPolicy
196
+ # ...
197
+
198
+ def self.create_params(params)
199
+ params.require(:foo).permit(:name)
200
+ end
201
+ end
202
+
203
+ class FoosController < ApplicationController
204
+ def create_params
205
+ FooPolicy.create_params(params)
206
+ end
207
+ end
208
+ ```
209
+
210
+ ### Create and Update Policies
211
+
212
+ For defining your `can_create?` and `can_update?` policies, it's probably a
213
+ good idea to perform the following order of actions in your controllers:
214
+
215
+ ```ruby
216
+ foo.assign_attributes(update_params)
217
+ authorize! :update, foo
218
+ foo.save!
219
+ ```
220
+
221
+ This way, in your `can_update?` policy you can check foo's `#changes`, etc.,
222
+ methods to see what was changed in case a user might only be allowed to modify
223
+ some fields but not others, or only make certain kinds of changes.
224
+
225
+ ### DRYing Up Your Controllers
226
+
227
+ You might be worried about missing out on CanCan's magical controller methods
228
+ that fetch and authorize your records for you.
229
+
230
+ But why not instead just define a method like this in `ApplicationController`?
231
+
232
+ ```ruby
233
+ def find_and_authorize(model_class, id, action)
234
+ record = model_class.find(id)
235
+ authorize! action, record
236
+ record
237
+ end
238
+ ```
239
+
240
+ Now you can just,
241
+
242
+ ```ruby
243
+ foo = find_and_authorize(Foo, params[:id], :read)
244
+ ```
245
+
246
+ You could define something analogous for updating and creating records,
247
+
248
+ ```ruby
249
+ def authorize_and_create(model_class, params)
250
+ record = model_class.new(params)
251
+ authorize! :create, record
252
+ [record.save, record]
253
+ end
254
+ ```
255
+
256
+ And in your actions use,
257
+
258
+ ```ruby
259
+ success, foo = authorize_and_create(Foo, create_params)
260
+
261
+ if success
262
+ # ...
263
+ else
264
+ # ...
265
+ end
266
+ ```
267
+
268
+ ## Complete-ish Example
269
+
270
+ ```ruby
271
+ # config/application.rb
272
+ # ...
273
+ config.autoload_paths += %W(#{config.root}/authorization)
274
+ # ...
275
+
276
+ # app/controllers/application_controller.rb
277
+ require 'kant/all'
278
+
279
+ class ApplicationController < ActionController::Base
280
+ include Kant::ControllerMixin
281
+
282
+ def current_access_control
283
+ if !current_user
284
+ Kant::NoAccess.new(nil)
285
+ elsif current_user.admin?
286
+ Kant::AllAccess.new(nil)
287
+ else
288
+ Kant::PolicyAccess.new(current_user, policies_module: Policies)
289
+ end
290
+ end
291
+ end
292
+
293
+ # app/authorization/policies/foo_policy.rb
294
+ module Policies
295
+ module FooPolicy
296
+ def self.readable(foos, user)
297
+ foos.where(user_id: user.id)
298
+ end
299
+ end
300
+ end
301
+
302
+ # app/controllers/foos_controller.rb
303
+ class FoosController < ApplicationController
304
+ def index
305
+ foos = accessible(:read, Foo)
306
+ render json: foos
307
+ end
308
+
309
+ def show
310
+ foo = Foo.find(params[:id])
311
+ authorize! :read, foo
312
+ render json: foo
313
+ end
314
+ end
315
+ ```
316
+
317
+ ## RSpec
318
+
319
+ Kant has RSpec matchers if you want to use them. Example:
320
+
321
+ ```ruby
322
+ # spec_helper.rb
323
+ require 'kant/rspec/matchers'
324
+
325
+ # foo_spec.rb
326
+ describe Foo
327
+ it "bars" do
328
+ # ... some setup ...
329
+ access_control = AccessControl.new(user)
330
+ expect(access_control).to be_able_to(:read, foo)
331
+ end
332
+ end
333
+ ```
334
+
335
+ ## But is it good?
336
+
337
+ It's small, simple, and it works. I use it in production. So maybe?
338
+
339
+ ## Why should I use this instead of CanCan?
340
+
341
+ First of all, this library is tiny.
342
+
343
+ ```bash
344
+ $ cat lib/kant/**/*.rb | wc -l
345
+ 212
346
+ ```
347
+
348
+ And this includes the RSpec matcher definition which you won't even use in
349
+ production. Excluding that, Kant is about 170 lines of code.
350
+
351
+ Compare this to CanCanCan:
352
+
353
+ ```bash
354
+ $ cat lib/**/*.rb | wc -l
355
+ 1849
356
+ ```
357
+
358
+ Also, it's fast:
359
+
360
+ **Warning: Do not trust these benchmarks. Perform your own measurements on your
361
+ own application.**
362
+
363
+ A problem with CanCan as your application grows is that it expects you to
364
+ define *all* of your abilities upon initialization, even if the request only
365
+ checks a single one. This is fine if you only have a couple of models, or if
366
+ your scopes are simple, but if your application has a couple dozen models, this
367
+ can translate into a significant overhead.
368
+
369
+ Here is a simple benchmark I executed on production when we still used CanCan:
370
+
371
+ ```ruby
372
+ require 'benchmark'
373
+ u = User.first
374
+
375
+ puts Benchmark.measure {
376
+ 10000.times { Ability.new(u) }
377
+ }
378
+ # => 48.510000 0.990000 49.500000 ( 70.699697)
379
+ ```
380
+
381
+ That's almost 50 seconds of CPU time simply to instantiate 10,000 objects.
382
+
383
+ Here is the benchmark from the application after it was refactored to use Kant:
384
+
385
+ ```ruby
386
+ require 'benchmark'
387
+ u = User.first
388
+
389
+ puts Benchmark.measure {
390
+ 10000.times { AccessControl.new(u) }
391
+ }
392
+ # => 0.060000 0.010000 0.070000 ( 0.169156)
393
+ ```
394
+
395
+ That's a *slight* difference. For us, this translates into a savings of roughly
396
+ 50 seconds of CPU time for every 10,000 requests.
397
+
398
+ **Perform your own benchmarks.**
399
+
400
+ ## Contributing
401
+
402
+ 1. Fork it ( https://github.com/markprzepiora/kant/fork )
403
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
404
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
405
+ 4. Push to the branch (`git push origin my-new-feature`)
406
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/kant.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kant/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kant"
8
+ spec.version = Kant::VERSION
9
+ spec.authors = ["Mark Przepiora"]
10
+ spec.email = ["mark.przepiora@gmail.com"]
11
+ spec.summary = %q{A tiny, non-magical authorization library}
12
+ spec.description = %q{Kant is a tiny authorization library for your Ruby (especially Rails and/or ActiveRecord) projects.}
13
+ spec.homepage = "https://github.com/markprzepiora/kant"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.1.0"
24
+ spec.add_development_dependency "with_model", "~> 1.2.1"
25
+ spec.add_development_dependency "sqlite3", "~> 1.3.10"
26
+ spec.add_development_dependency "activerecord", ">= 3.2"
27
+ end
@@ -0,0 +1,4 @@
1
+ module Kant
2
+ class AccessDenied < StandardError
3
+ end
4
+ end
data/lib/kant/all.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'kant/all_access'
2
+ require 'kant/no_access'
3
+ require 'kant/policy_access'
4
+ require 'kant/controller_mixin'
@@ -0,0 +1,14 @@
1
+ module Kant
2
+ class AllAccess
3
+ def initialize(user)
4
+ end
5
+
6
+ def can?(action, object)
7
+ true
8
+ end
9
+
10
+ def accessible(action, scope)
11
+ scope.all
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ require 'kant/access_denied'
2
+
3
+ module Kant
4
+ module ControllerMixin
5
+ # By default, Kant expects an AccessControl class to exist. Override this
6
+ # method if you need more complicated logic here. A typical implementation
7
+ # might be:
8
+ #
9
+ # def current_access_control
10
+ # @current_access_control ||=
11
+ # if !current_user
12
+ # Kant::NoAccess.new(nil)
13
+ # elsif current_user.admin?
14
+ # Kant::AllAccess.new(current_user)
15
+ # else
16
+ # AccessControl.new(current_user)
17
+ # end
18
+ # end
19
+ def current_access_control
20
+ @current_access_control ||= AccessControl.new(current_user)
21
+ end
22
+
23
+ private
24
+
25
+ # Delegates to current_access_control
26
+ def can?(*args)
27
+ current_access_control.can?(*args)
28
+ end
29
+
30
+ # Delegates to current_access_control
31
+ def accessible(*args)
32
+ current_access_control.accessible(*args)
33
+ end
34
+
35
+ # If `can?(action, object)` is true, then this is a no-op. If on the other
36
+ # hand that value is false, this raises a Kant::AccessDenied exception.
37
+ def authorize!(action, object)
38
+ if can?(action, object)
39
+ true
40
+ else
41
+ raise Kant::AccessDenied, "You are not permitted to #{action} this record."
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ module Kant
2
+ class NoAccess
3
+ def initialize(user)
4
+ end
5
+
6
+ def can?(action, object)
7
+ false
8
+ end
9
+
10
+ def accessible(action, scope)
11
+ scope.none
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,69 @@
1
+ require 'kant/resolvers/active_record'
2
+
3
+ module Kant
4
+ class PolicyAccess
5
+ include Kant::Resolvers::ActiveRecord
6
+
7
+ attr_accessor :user
8
+
9
+ def initialize(user, policies_module: nil)
10
+ @user = user
11
+ @policies_module = policies_module || Kernel
12
+ end
13
+
14
+ # Delegates to an appropriate Policy module. For example,
15
+ #
16
+ # Ability.new(user).can?(:read, Foo.first)
17
+ #
18
+ # will return
19
+ #
20
+ # FooPolicy.can_read?(Foo.first, user)
21
+ def can?(action, object)
22
+ method_eh = "can_#{action}?"
23
+ abilities = resolve_object(object)
24
+ _scope_method = scope_method(abilities, action)
25
+ model_class = object.class
26
+
27
+ if abilities.respond_to?(method_eh)
28
+ abilities.send(method_eh, object, user)
29
+ elsif _scope_method && object.id
30
+ abilities.send(_scope_method, model_class, user).where(id: object.id).any?
31
+ else
32
+ false
33
+ end
34
+ end
35
+
36
+ # Example:
37
+ #
38
+ # ability.accessible(:read, Content)
39
+ # # => a Content scope
40
+ def accessible(action, scope)
41
+ abilities = resolve_scope(scope)
42
+ _scope_method = scope_method(abilities, action)
43
+
44
+ if _scope_method
45
+ abilities.send(_scope_method, scope, user)
46
+ else
47
+ scope.none
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def scope_method(abilities, action)
54
+ regular_name = "#{action}able"
55
+ alternate_name = begin
56
+ first, *rest = action.to_s.split('_')
57
+ "#{first}able_#{rest.join('_')}"
58
+ end
59
+
60
+ if abilities.respond_to?(regular_name)
61
+ regular_name
62
+ elsif abilities.respond_to?(alternate_name)
63
+ alternate_name
64
+ else
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,30 @@
1
+ module Kant
2
+ module Resolvers
3
+ module ActiveRecord
4
+ private
5
+
6
+ attr_reader :policies_module
7
+
8
+ def resolve_object(object_or_class)
9
+ klass =
10
+ if object_or_class.is_a?(Class)
11
+ object_or_class
12
+ else
13
+ object_or_class.class
14
+ end
15
+
16
+ resolve(klass.name)
17
+ end
18
+
19
+ def resolve_scope(scope)
20
+ resolve(scope.all.model.name)
21
+ end
22
+
23
+ def resolve(name)
24
+ policies_module.const_get("#{name}Policy")
25
+ rescue NameError
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # These matchers are taken directly from CanCanCan:
2
+ # https://github.com/CanCanCommunity/cancancan/blob/develop/lib/cancan/matchers.rb
3
+
4
+ rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # for RSpec 1 compatability
5
+
6
+ if rspec_module == 'RSpec'
7
+ require 'rspec/core'
8
+ require 'rspec/expectations'
9
+ end
10
+
11
+ Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
12
+ match do |ability|
13
+ ability.can?(*args)
14
+ end
15
+
16
+ # Check that RSpec is < 2.99
17
+ if !respond_to?(:failure_message) && respond_to?(:failure_message_for_should)
18
+ alias :failure_message :failure_message_for_should
19
+ alias :failure_message_when_negated :failure_message_for_should_not
20
+ end
21
+
22
+ failure_message do |ability|
23
+ "expected to be able to #{args.map(&:inspect).join(" ")}"
24
+ end
25
+
26
+ failure_message_when_negated do |ability|
27
+ "expected not to be able to #{args.map(&:inspect).join(" ")}"
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Kant
2
+ VERSION = "0.0.2"
3
+ end
data/lib/kant.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "kant/version"
2
+
3
+ module Kant
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+ require 'kant/all_access'
3
+
4
+ describe Kant::AllAccess do
5
+ let(:user) { double("user") }
6
+ subject(:all_access) { Kant::AllAccess.new(user) }
7
+
8
+ it "it can do anything" do
9
+ foo = double("foo")
10
+
11
+ expect(all_access).to be_able_to(:bar, foo)
12
+ end
13
+
14
+ it "it has access to everything" do
15
+ scope = double("scope")
16
+ expect(scope).to receive(:all).and_return(scope)
17
+
18
+ expect(all_access.accessible(:foo, scope)).to eq(scope)
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'kant/all_access'
3
+
4
+ describe Kant do
5
+ setup_models
6
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ require 'kant/no_access'
3
+
4
+ describe Kant::NoAccess do
5
+ let(:user) { double("user") }
6
+ subject(:no_access) { Kant::NoAccess.new(user) }
7
+
8
+ it "it can't do anything" do
9
+ foo = double("foo")
10
+
11
+ expect(no_access).not_to be_able_to(:bar, foo)
12
+ end
13
+
14
+ it "it has access to nothing" do
15
+ scope = double("scope")
16
+ none = double("none")
17
+ expect(scope).to receive(:none).and_return(none)
18
+
19
+ expect(no_access.accessible(:foo, scope)).to eq(none)
20
+ end
21
+ end
@@ -0,0 +1,155 @@
1
+ require 'spec_helper'
2
+ require 'kant/policy_access'
3
+
4
+ describe Kant::PolicyAccess do
5
+ setup_models
6
+
7
+ describe "#can?" do
8
+ let(:user) { User.create!(email: 'foo@bar.com') }
9
+ subject(:policy_access) { Kant::PolicyAccess.new(user) }
10
+
11
+ it "uses FooPolicy to authorize Foos" do
12
+ todo = Todo.create
13
+ bell = double("bell")
14
+ expect(bell).to receive(:ring).with(todo, user).and_return(nil)
15
+
16
+ # module TodoPolicy
17
+ # def self.can_tickle?(todo, user)
18
+ # bell.ring(todo, user)
19
+ # "foo"
20
+ # end
21
+ # end
22
+ todo_policy = Class.new do
23
+ define_singleton_method(:can_tickle?) do |todo, user|
24
+ bell.ring(todo, user)
25
+ "foo"
26
+ end
27
+ end
28
+
29
+ stub_const("TodoPolicy", todo_policy)
30
+
31
+ expect(policy_access.can?(:tickle, todo)).to eq("foo")
32
+ expect(policy_access.can?(:read, todo)).to eq(false)
33
+ end
34
+
35
+ it "uses FooPolicy to authorize Foo itself" do
36
+ bell = double("bell")
37
+ expect(bell).to receive(:ring).with(Todo, user).and_return(nil)
38
+
39
+ # module TodoPolicy
40
+ # def self.can_tickle?(todo, user)
41
+ # bell.ring(todo, user)
42
+ # "foo"
43
+ # end
44
+ # end
45
+ todo_policy = Class.new do
46
+ define_singleton_method(:can_tickle?) do |todo, user|
47
+ bell.ring(todo, user)
48
+ "foo"
49
+ end
50
+ end
51
+
52
+ stub_const("TodoPolicy", todo_policy)
53
+
54
+ expect(policy_access.can?(:tickle, Todo)).to eq("foo")
55
+ end
56
+
57
+ it "returns false for an undefined action" do
58
+ todo = Todo.create
59
+ stub_const("TodoPolicy", Class.new)
60
+
61
+ expect(policy_access.can?(:tickle, todo)).to eq(false)
62
+ end
63
+
64
+ it "returns false if FooPolicy does not exist" do
65
+ todo = Todo.create
66
+ expect(policy_access.can?(:tickle, todo)).to eq(false)
67
+ end
68
+
69
+ it "uses an *able scope method if no can_*? method exists" do
70
+ # module TodoPolicy
71
+ # def self.readable(todos, user)
72
+ # todos.where(completed: true)
73
+ # end
74
+ # end
75
+ todo_policy = Class.new do
76
+ define_singleton_method(:readable) do |todos, user|
77
+ todos.where(completed: true)
78
+ end
79
+ end
80
+
81
+ stub_const("TodoPolicy", todo_policy)
82
+
83
+ complete_todo = Todo.create!(completed: true)
84
+ incomplete_todo = Todo.create!(completed: false)
85
+
86
+ expect(policy_access).to be_able_to(:read, complete_todo)
87
+ expect(policy_access).not_to be_able_to(:read, incomplete_todo)
88
+
89
+ expect(policy_access).not_to be_able_to(:tickle, complete_todo)
90
+ expect(policy_access).not_to be_able_to(:tickle, incomplete_todo)
91
+ end
92
+ end
93
+
94
+ describe "#accessible" do
95
+ let(:user) { User.create!(email: 'foo@bar.com') }
96
+ subject(:policy_access) { Kant::PolicyAccess.new(user) }
97
+
98
+ it "delegates to a Policy module" do
99
+ # module TodoPolicy
100
+ # def self.readable(todos, user)
101
+ # todos.where(completed: true)
102
+ # end
103
+ # end
104
+ todo_policy = Class.new do
105
+ define_singleton_method(:readable) do |todos, user|
106
+ todos.where(completed: true)
107
+ end
108
+ end
109
+
110
+ stub_const("TodoPolicy", todo_policy)
111
+
112
+ complete_todo = Todo.create!(completed: true)
113
+ incomplete_todo = Todo.create!(completed: false)
114
+
115
+ expect(policy_access.accessible(:read, Todo)).to eq([complete_todo])
116
+ expect(policy_access.accessible(:read, Todo.where(completed: false))).to eq([])
117
+ end
118
+
119
+ it "returns an empty scope when no scope method is defined" do
120
+ stub_const("TodoPolicy", Class.new)
121
+
122
+ complete_todo = Todo.create!(completed: true)
123
+ incomplete_todo = Todo.create!(completed: false)
124
+
125
+ expect(policy_access.accessible(:read, Todo)).to eq(Todo.none)
126
+ end
127
+ end
128
+
129
+ describe "the policies_module option in initializer" do
130
+ let(:user) { User.create!(email: 'foo@bar.com') }
131
+
132
+ it "can be specified to namespace policies" do
133
+ # module Policies::TodoPolicy
134
+ # def self.readable(todos, user)
135
+ # todos.where(completed: true)
136
+ # end
137
+ # end
138
+ todo_policy = Class.new do
139
+ define_singleton_method(:readable) do |todos, user|
140
+ todos.where(completed: true)
141
+ end
142
+ end
143
+
144
+ stub_const("Policies", Module.new)
145
+ stub_const("Policies::TodoPolicy", todo_policy)
146
+
147
+ policy_access = Kant::PolicyAccess.new(user, policies_module: Policies)
148
+
149
+ complete_todo = Todo.create!(completed: true)
150
+ incomplete_todo = Todo.create!(completed: false)
151
+
152
+ expect(policy_access.accessible(:read, Todo)).to eq([complete_todo])
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,87 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'active_record'
5
+ require 'sqlite3'
6
+ require 'with_model'
7
+ require 'kant/rspec/matchers'
8
+ require 'kant'
9
+
10
+ ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
11
+
12
+ RSpec.configure do |config|
13
+ config.extend WithModel
14
+ end
15
+
16
+ def setup_models
17
+ with_model :User do
18
+ table do |t|
19
+ t.string "email"
20
+ end
21
+
22
+ model do
23
+ has_many :projects_as_owner,
24
+ :class_name => 'Project',
25
+ :foreign_key => :owner_id,
26
+ :inverse_of => :owner
27
+
28
+ has_many :authored_todos,
29
+ :foreign_key => :author_id,
30
+ :inverse_of => :author,
31
+ :class_name => 'Todo'
32
+
33
+ has_and_belongs_to_many :projects_as_developer,
34
+ :class_name => 'Project',
35
+ :join_table => 'project_developers'
36
+ end
37
+ end
38
+
39
+ with_model :Project do
40
+ table do |t|
41
+ t.string "name"
42
+ t.integer "owner_id"
43
+ t.datetime "created_at", :null => false
44
+ t.datetime "updated_at", :null => false
45
+ end
46
+
47
+ model do
48
+ belongs_to :owner,
49
+ :class_name => 'User',
50
+ :inverse_of => :projects_as_owner
51
+
52
+ has_many :todos,
53
+ :inverse_of => :project
54
+
55
+ has_and_belongs_to_many :developers,
56
+ :class_name => 'User',
57
+ :join_table => 'project_developers'
58
+ end
59
+ end
60
+
61
+ with_table "project_developers", :id => false do |t|
62
+ t.integer "user_id", :null => false
63
+ t.integer "project_id", :null => false
64
+ end
65
+
66
+ with_model :Todo do
67
+ table do |t|
68
+ t.string "name"
69
+ t.string "type"
70
+ t.boolean "completed"
71
+ t.integer "suggested_by_id"
72
+ t.integer "author_id"
73
+ t.integer "project_id"
74
+ t.datetime "created_at", :null => false
75
+ t.datetime "updated_at", :null => false
76
+ end
77
+
78
+ model do
79
+ belongs_to :project,
80
+ :inverse_of => :todos
81
+
82
+ belongs_to :author,
83
+ :inverse_of => :authored_todos,
84
+ :class_name => 'User'
85
+ end
86
+ end
87
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Mark Przepiora
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.1.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: with_model
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.2.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.3.10
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.3.10
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.2'
97
+ description: Kant is a tiny authorization library for your Ruby (especially Rails
98
+ and/or ActiveRecord) projects.
99
+ email:
100
+ - mark.przepiora@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - Gemfile
107
+ - LICENSE.txt
108
+ - README.md
109
+ - Rakefile
110
+ - kant.gemspec
111
+ - lib/kant.rb
112
+ - lib/kant/access_denied.rb
113
+ - lib/kant/all.rb
114
+ - lib/kant/all_access.rb
115
+ - lib/kant/controller_mixin.rb
116
+ - lib/kant/no_access.rb
117
+ - lib/kant/policy_access.rb
118
+ - lib/kant/resolvers/active_record.rb
119
+ - lib/kant/rspec/matchers.rb
120
+ - lib/kant/version.rb
121
+ - spec/kant/all_access_spec.rb
122
+ - spec/kant/integration_spec.rb
123
+ - spec/kant/no_access_spec.rb
124
+ - spec/kant/policy_access_spec.rb
125
+ - spec/spec_helper.rb
126
+ homepage: https://github.com/markprzepiora/kant
127
+ licenses:
128
+ - MIT
129
+ metadata: {}
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubyforge_project:
146
+ rubygems_version: 2.2.2
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: A tiny, non-magical authorization library
150
+ test_files:
151
+ - spec/kant/all_access_spec.rb
152
+ - spec/kant/integration_spec.rb
153
+ - spec/kant/no_access_spec.rb
154
+ - spec/kant/policy_access_spec.rb
155
+ - spec/spec_helper.rb