lawkeeper 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ *.rvmrc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lawkeeper.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Brendon Murphy
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,178 @@
1
+ # Lawkeeper
2
+
3
+ Lawkeeper - Simple authorization policies for Rack apps
4
+
5
+ Lawkeeper was heavily inspired by the Pundit authorization gem. Lawkeeper
6
+ follows a very similar pattern, but is more agnostic and geared towards use
7
+ in smaller Rack applications.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'lawkeeper'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install lawkeeper
22
+
23
+ ## Usage
24
+
25
+ Lawkeeper makes a couple basic assumptions
26
+
27
+ * You have a `current_user` helper
28
+ * You create policy files like `PostPolicy` for a `Post` model
29
+ * You have a headers method with a settable hash for response headers
30
+
31
+ After setting up your model policies, include `Lawkeeper::Helpers`
32
+ into your app. Let's assume Sinatra as our example:
33
+
34
+ ```ruby
35
+ helpers do
36
+ include Lawkeeper::Helpers
37
+ end
38
+ ```
39
+
40
+ This provides a few useful helpers:
41
+
42
+ * `can?` - for checking if the current_user is permitted an action on the
43
+ record
44
+ * `authorize` - checks if the user can perform the action, otherwise raise
45
+ `Lawkeeper::NotAuthorized`
46
+ * `skip_authorization` - used to flag an action as not needing authorization
47
+
48
+ ### Declaring policy classes
49
+
50
+ By default, Lawkeeper follows a convention of mapping policy classes like
51
+ `PostPolicy` for a `Post` class, `CommentPolicy` for `Comment`, etc.
52
+
53
+ The simplest way to declare a post policy is inherit `Lawkeeper::Policy`
54
+ and declare predicates for policy checks:
55
+
56
+ ```ruby
57
+ class PostPolicy < Lawkeeper::Policy
58
+ def read?
59
+ true
60
+ end
61
+
62
+ def update?
63
+ record.owned_by?(user)
64
+ end
65
+ end
66
+ ```
67
+
68
+ Lawkeeper makes no assumptions about the name of your policy queries. You can
69
+ call them `show?` or `read?`, `delete?` or `destroy?`, whichever you prefer. The
70
+ only requirement is that they end with '?'.
71
+
72
+ Policy classes are instantiated with the current user and a record for checking.
73
+
74
+ If you wish to use an unconventially named Policy class for a model, add the
75
+ `#policy_class` instance method to your model. For example:
76
+
77
+ ```ruby
78
+ class Post
79
+ def policy_class
80
+ OwnershipPolicy
81
+ end
82
+ end
83
+ ```
84
+
85
+ Lawkeeper helper methods will prefer the `#policy_class` specified if it exists.
86
+
87
+ ### Authorizing in actions
88
+
89
+ To authorize in a controller action is simple:
90
+
91
+ ```ruby
92
+ get "/post/:id" do
93
+ @post = Post.find(id)
94
+ authorize @post, :read
95
+ erb :post_show
96
+ end
97
+ ```
98
+
99
+ If authorize is permitted (which it usually should be) the action will continue
100
+ as normal. If it fails, Lawkeeper::NotAuthorized will be raised.
101
+
102
+ ### Checking in views
103
+
104
+ Lawkeeper provides a `can?` helper to use in your views:
105
+
106
+ ```ruby
107
+ <% if can? :edit, @post %>
108
+ <a href="/posts/<%= @post.id %>/edit">Edit Post</a>
109
+ <% end %>
110
+ ```
111
+
112
+ The `can?` method is a check, it will not raise authorization exceptions.
113
+
114
+ ### Specifying policy classes
115
+
116
+ If you wish to specify a policy class at runtime for a call to `can?` or `authorize`,
117
+ you can pass a policy class as an option third argument.
118
+
119
+ ```ruby
120
+ authorize @post, :read, OwnershipPolicy
121
+ ```
122
+
123
+ ## Ensuring authorization with middlewares
124
+
125
+ Lawkeeper provides `EnsureWare` for checking that authorization was performed
126
+ for all actions. When the `authorize` or `skip_authorization` methods are
127
+ employed in actions, response headers are set. The middleware then checks
128
+ and deletes the headers. If the header was not present, a 403 forbidden status
129
+ will be returned.
130
+
131
+ This is useful to ensure you do not forget to authorize the resource in any
132
+ given action.
133
+
134
+ If you do not wish to enforce such a check, you should employ the `ScrubWare`
135
+ middleware instead. This is simply responsible for stripping Lawkeeper headers
136
+ before sending the response on its way.
137
+
138
+ If you'd prefer to not use middleware at all, it's advised you set Lawkeeper to
139
+ simply skip the setting of headers:
140
+
141
+ ```ruby
142
+ Lawkeeper.skip_set_headers = true
143
+ ```
144
+
145
+ This will not prevent how Lawkeeper does its primary job of authorizing policy
146
+ actions.
147
+
148
+ ## Outstanding Problems
149
+
150
+ Lawkeeper as yet has no mechanism for scoping finds for collection 'index' like
151
+ methods. Pundit (which Lawkeeper is influenced by) has some similiar problems
152
+ in this area as well (though it does provide a scope object).
153
+
154
+ The primary challenges are:
155
+
156
+ * A collection action is very different than an instance authorization, because
157
+ you don't authorize instances but rather find them via policy
158
+ * As compared to instance authorization, scoped finding is very coupled to an
159
+ ORM, model, or storage pattern in a given application.
160
+
161
+ My immediate thinking for Lawkeeper is to call `skip_authorization` in actions
162
+ where you have performed a scoped find. This will likely keep development in check
163
+ since adding the call is a mental note for "hey this should be permitted records only"
164
+
165
+ To build upon this, I believe either:
166
+
167
+ * The scoping should be up to you, or
168
+ * The scope permission handling should delegate to third party ORM specific plugins.
169
+
170
+ I am still rolling around this in my head, with the goal of keeping it simple
171
+
172
+ ## Contributing
173
+
174
+ 1. Fork it
175
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
176
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
177
+ 4. Push to the branch (`git push origin my-new-feature`)
178
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:spec) do |t|
6
+ t.libs.push "lib"
7
+ t.libs.push "spec"
8
+ t.test_files = FileList['spec/*_spec.rb']
9
+ t.verbose = true
10
+ end
11
+
data/lawkeeper.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/lawkeeper/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Brendon Murphy"]
6
+ gem.email = ["xternal1+github@gmail.com"]
7
+ gem.summary = %q{Lawkeeper - Simple authorization policies for Rack apps}
8
+ gem.description = gem.summary
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "lawkeeper"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Lawkeeper::VERSION
17
+
18
+ gem.add_development_dependency "minitest"
19
+ end
@@ -0,0 +1,3 @@
1
+ module Lawkeeper
2
+ VERSION = "0.0.1"
3
+ end
data/lib/lawkeeper.rb ADDED
@@ -0,0 +1,102 @@
1
+ require "lawkeeper/version"
2
+
3
+ module Lawkeeper
4
+ AUTHORIZED_HEADER = 'Lawkeeper-Authorized'.freeze
5
+ SKIPPED_HEADER = 'Lawkeeper-Skipped'.freeze
6
+
7
+ class NotAuthorized < StandardError; end
8
+ class NotDefined < StandardError; end
9
+
10
+ class << self
11
+ attr_accessor :skip_set_headers
12
+ end
13
+
14
+ class Policy
15
+ attr_reader :user, :record
16
+
17
+ def initialize(user, record)
18
+ @user = user
19
+ @record = record
20
+ end
21
+ end
22
+
23
+ class PolicyLookup
24
+ def self.[](model)
25
+ if model.respond_to?(:policy_class)
26
+ model.policy_class
27
+ else
28
+ begin
29
+ Object.const_get("#{model.class}Policy")
30
+ rescue NameError
31
+ raise NotDefined
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ module Helpers
38
+ def can?(action, model, policy_class = nil)
39
+ policy_class ||= Lawkeeper::PolicyLookup[model]
40
+ policy_method = "#{action}?"
41
+ policy_class.new(current_user, model).public_send(policy_method)
42
+ end
43
+
44
+ def authorize(model, action, policy_class = nil)
45
+ if can?(action, model, policy_class)
46
+ set_lawkeeper_header(AUTHORIZED_HEADER)
47
+ else
48
+ raise NotAuthorized
49
+ end
50
+ end
51
+
52
+ def skip_authorization
53
+ set_lawkeeper_header(SKIPPED_HEADER)
54
+ end
55
+
56
+ def set_lawkeeper_header(header)
57
+ headers[header] = 'true' unless Lawkeeper.skip_set_headers
58
+ end
59
+ end
60
+
61
+ class EnsureWare
62
+ def initialize(app, options = {})
63
+ @app = app
64
+ @options = options
65
+ end
66
+
67
+ def call(env)
68
+ dup._call(env)
69
+ end
70
+
71
+ def _call(env)
72
+ status, headers, body = @app.call(env)
73
+
74
+ if headers.delete(AUTHORIZED_HEADER) || headers.delete(SKIPPED_HEADER)
75
+ [status, headers, body]
76
+ else
77
+ [status_code, {"Content-Type" => "text/plain"}, ['forbidden, authorization required']]
78
+ end
79
+ end
80
+
81
+ def status_code
82
+ @options.fetch(:status_code, 403)
83
+ end
84
+ end
85
+
86
+ class ScrubWare
87
+ def initialize(app)
88
+ @app = app
89
+ end
90
+
91
+ def call(env)
92
+ dup._call(env)
93
+ end
94
+
95
+ def _call(env)
96
+ status, headers, body = @app.call(env)
97
+ headers.delete(AUTHORIZED_HEADER)
98
+ headers.delete(SKIPPED_HEADER)
99
+ [status, headers, body]
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ class User
4
+ attr_accessor :permit
5
+ end
6
+
7
+ class Comment
8
+ attr_accessor :permit
9
+ end
10
+
11
+ class CommentPolicy < Lawkeeper::Policy
12
+ def read?
13
+ user.permit && record.permit
14
+ end
15
+ end
16
+
17
+ describe Lawkeeper::Helpers do
18
+ let(:comment) { Comment.new }
19
+ let(:controller) {
20
+ c = Object.new.tap { |c| c.extend(Lawkeeper::Helpers) }
21
+ def c.current_user
22
+ @current_user ||= User.new
23
+ end
24
+
25
+ def c.headers
26
+ @headers ||= {}
27
+ end
28
+
29
+ c
30
+ }
31
+
32
+ def permit_action
33
+ controller.current_user.permit = true
34
+ comment.permit = true
35
+ end
36
+
37
+ describe '#can?' do
38
+ it "returns false for a user denied action" do
39
+ controller.current_user.permit = true
40
+ comment.permit = false
41
+ controller.can?(:read, comment).must_equal false
42
+
43
+ controller.current_user.permit = false
44
+ comment.permit = true
45
+ controller.can?(:read, comment).must_equal false
46
+ end
47
+
48
+ it "returns true for a user permitted action" do
49
+ permit_action
50
+ controller.can?(:read, comment).must_equal true
51
+ end
52
+
53
+ it "can receive a specified policy class" do
54
+ klass = Class.new(Lawkeeper::Policy) do
55
+ def read?
56
+ true
57
+ end
58
+ end
59
+ controller.can?(:read, comment, klass).must_equal true
60
+ end
61
+ end
62
+
63
+ describe '#authorize' do
64
+ it "sets the authorized header to 'true' if the user can run the action" do
65
+ permit_action
66
+ controller.authorize(comment, :read)
67
+ controller.headers['Lawkeeper-Authorized'].must_equal 'true'
68
+ end
69
+
70
+ it "skips setting the header if Lawkeeper.skip_set_headers is true" do
71
+ Lawkeeper.skip_set_headers = true
72
+
73
+ permit_action
74
+ controller.authorize(comment, :read)
75
+ controller.headers['Lawkeeper-Authorized'].must_be_nil 'true'
76
+
77
+ Lawkeeper.skip_set_headers = nil
78
+ end
79
+
80
+ it "raises NotAuthorized if the user is not permitted the action" do
81
+ lambda {
82
+ controller.authorize(comment, :read)
83
+ }.must_raise(Lawkeeper::NotAuthorized)
84
+ end
85
+
86
+ it "can receive a specified policy class" do
87
+ klass = Class.new(Lawkeeper::Policy) do
88
+ def read?
89
+ true
90
+ end
91
+ end
92
+ controller.authorize(comment, :read, klass)
93
+ end
94
+ end
95
+
96
+ describe '#skip_authorization' do
97
+ it "sets the authorized header to 'true'" do
98
+ controller.skip_authorization
99
+ controller.headers['Lawkeeper-Skipped'].must_equal 'true'
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lawkeeper, 'middlewares' do
4
+ describe Lawkeeper::EnsureWare do
5
+ it "returns the result of calling the app if Lawkeeper-Authorized is 'true'"
6
+ it "returns the result of calling the app if Lawkeeper-Skipped is 'true'"
7
+ it "returns the result of calling the app if no Lawkeeper header is set"
8
+ it "deletes the lawkeeper headers"
9
+ end
10
+
11
+ describe Lawkeeper::ScrubWare do
12
+ it "deletes the lawkeeper headers"
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ class Post; end
4
+ class PostPolicy; end
5
+ class SpecifiedPolicy; end
6
+
7
+ describe Lawkeeper::PolicyLookup do
8
+ it "returns PostPolicy for a Post instance" do
9
+ Lawkeeper::PolicyLookup[Post.new].must_equal PostPolicy
10
+ end
11
+
12
+ it "prefers the instance#policy_class if available" do
13
+ post = Post.new
14
+ def post.policy_class
15
+ SpecifiedPolicy
16
+ end
17
+ Lawkeeper::PolicyLookup[post].must_equal SpecifiedPolicy
18
+ end
19
+
20
+ it "raises NotDefined if the policy can not be found" do
21
+ lambda {
22
+ Lawkeeper::PolicyLookup[Object.new]
23
+ }.must_raise(Lawkeeper::NotDefined)
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ require "spec_helper"
2
+
3
+ describe Lawkeeper::Policy do
4
+ it "is initialized with a user and record" do
5
+ policy = Lawkeeper::Policy.new(:user, :record)
6
+ policy.user.must_equal :user
7
+ policy.record.must_equal :record
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ require "minitest/autorun"
2
+ require "lawkeeper"
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lawkeeper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brendon Murphy
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: &2152801400 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2152801400
25
+ description: Lawkeeper - Simple authorization policies for Rack apps
26
+ email:
27
+ - xternal1+github@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lawkeeper.gemspec
38
+ - lib/lawkeeper.rb
39
+ - lib/lawkeeper/version.rb
40
+ - spec/helpers_spec.rb
41
+ - spec/middleware_spec.rb
42
+ - spec/policy_lookup_spec.rb
43
+ - spec/policy_spec.rb
44
+ - spec/spec_helper.rb
45
+ homepage: ''
46
+ licenses: []
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.15
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Lawkeeper - Simple authorization policies for Rack apps
69
+ test_files:
70
+ - spec/helpers_spec.rb
71
+ - spec/middleware_spec.rb
72
+ - spec/policy_lookup_spec.rb
73
+ - spec/policy_spec.rb
74
+ - spec/spec_helper.rb