lawkeeper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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