kant 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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