regulator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ before_install: gem install bundler -v 1.10.5
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.5
6
+ - 2.2.0
7
+ - jruby-19mode
8
+ - rbx-2
9
+ env:
10
+ - RSPEC_VERSION="<2.99"
11
+ - RSPEC_VERSION="~>3.0
12
+ - CODECLIMATE_REPO_TOKEN=561cc65a0f136c90b4661c76c96e4ca0717d49a1336cb08d7a617f2cb7d4f6c6
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Regulator
2
+
3
+ ## 0.1.0 (2015-07-23)
4
+ - initial release
5
+ - pundit compatible
6
+ - controller based policy namespacing
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in regulator.gemspec
4
+ gem "rspec"
5
+ gem "codeclimate-test-reporter", group: :test, require: nil
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Cory O'Daniel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Regulator
2
+ [![Build Status](https://travis-ci.org/coryodaniel/regulator.svg)](https://travis-ci.org/coryodaniel/regulator)
3
+ [![Code Climate](https://codeclimate.com/github/coryodaniel/regulator/badges/gpa.svg)](https://codeclimate.com/github/coryodaniel/regulator)
4
+ [![Test Coverage](https://codeclimate.com/github/coryodaniel/regulator/badges/coverage.svg)](https://codeclimate.com/github/coryodaniel/regulator/coverage)
5
+
6
+ Regulator is a clone of the [Pundit](https://github.com/elabs/pundit) gem and provides a pundit compatible DSL that has **controller namespaced** authorization polices instead of *model namespaced*.
7
+
8
+ It uses Ruby classes and object oriented design patterns to build a simple, robust and scaleable authorization system.
9
+
10
+ Existing pundit policies can be used, although they will have to be namespaced properly, or have the controller accessing set ```Controller.policy_class``` or ```Controller.policy_namespace```
11
+
12
+ I built this because I believe authorization should be controller-based, not model based, but really enjoyed using the Pundit DSL and I was over [monkey-patching](https://gist.github.com/Systho/3d7632b5aa999cf88d87) pundit in all of my projects to make it work the way I want.
13
+
14
+ Why not contribute to pundit? [It's](https://github.com/elabs/pundit/issues/12) [been](https://github.com/elabs/pundit/issues/178) an [on going](https://github.com/elabs/pundit/search?q=namespace&type=Issues&utf8=%E2%9C%93) 'issue' in pundit and it doesn't look [like it'll be reality.](https://github.com/elabs/pundit/pull/190#issuecomment-53052356)
15
+
16
+ ## TODOs
17
+ * [ ] generators
18
+ * [ ] activeadmin-regulator-adapter gem or generator
19
+ * [ ] documentation (Usage section below, mock pundits)
20
+ * [ ] Lotus examples
21
+ * [ ] Grape examples
22
+ * [ ] ROM examples
23
+ * [ ] Custom permissions examples
24
+ * [ ] RoleModel gem examples
25
+ * [ ] rolify gem examples
26
+ * [ ] contributing wiki
27
+
28
+ ## Installation
29
+
30
+ Add this line to your application's Gemfile:
31
+
32
+ ```ruby
33
+ gem 'regulator'
34
+ ```
35
+
36
+ And then execute:
37
+
38
+ $ bundle
39
+
40
+ Or install it yourself as:
41
+
42
+ $ gem install regulator
43
+
44
+ ## Usage
45
+
46
+ TODO: Write usage instructions here
47
+
48
+ ## Development
49
+
50
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
51
+
52
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
53
+
54
+ ## License
55
+
56
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
57
+
58
+ ## Contributors
59
+ * [Cory O'Daniel](http://linkedin.com/in/coryodaniel)
60
+
61
+ Thanks to Warren G for the inspiration, bro.
62
+
63
+ ![Regulator](https://upload.wikimedia.org/wikipedia/commons/a/ac/Nat_Powers_%26_Warren_G.jpg)
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'yard'
5
+
6
+ desc "Run all examples"
7
+ RSpec::Core::RakeTask.new(:spec) do |t|
8
+ #t.rspec_path = 'bin/rspec'
9
+ t.rspec_opts = %w[--color]
10
+ end
11
+
12
+ YARD::Rake::YardocTask.new do |t|
13
+ t.files = ['lib/**/*.rb']
14
+ #t.options = ['--any', '--extra', '--opts'] # optional
15
+ end
16
+
17
+ task :default => [:spec]
data/bin/console ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "regulator"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+ class Project;end;
9
+ class User;end;
10
+
11
+ class ProjectPolicy < Struct.new(:user, :project);end;
12
+ class LegacyProjectPolicy < Struct.new(:user, :project);end;
13
+
14
+ class ApplicationController
15
+ def current_user
16
+ User.new
17
+ end
18
+ end
19
+
20
+ class ProjectsController < ApplicationController
21
+ include Regulator
22
+ end
23
+
24
+ module Api
25
+ class UserPolicy < Struct.new(:user, :record);end;
26
+
27
+ class BaseController < ApplicationController
28
+ include Regulator
29
+ end
30
+
31
+ module V2
32
+ class UserPolicy < Struct.new(:user, :record);end;
33
+ class ProjectsController < Api::BaseController
34
+ def self.policy_class
35
+ ProjectPolicy
36
+ end
37
+ end
38
+
39
+ class UsersController < Api::BaseController
40
+ end
41
+ end
42
+
43
+ class UsersController < Api::BaseController
44
+ end
45
+
46
+ class ProjectsController < Api::BaseController
47
+ def self.policy_class
48
+ LegacyProjectPolicy
49
+ end
50
+ end
51
+ end
52
+
53
+ ProjectsController.new.policy_namespace #=> nil
54
+ Api::ProjectsController.new.policy_namespace #=> Api
55
+ Api::V2::ProjectsController.new.policy_namespace #=> Api::V2
56
+
57
+ ProjectsController.new.policy(Project) #=> ProjectPolicy
58
+ Api::ProjectsController.new.policy(Project) #=> LegacyProjectPolicy
59
+ Api::V2::ProjectsController.new.policy(Project) #=> ProjectPolicy
60
+
61
+ Api::UsersController.new.policy(User) #=> Api::UserPolicy
62
+ Api::V2::UsersController.new.policy(User) #=> Api::V2::UserPolicy
63
+
64
+ require "pry"
65
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,80 @@
1
+ module Regulator
2
+ class PolicyFinder
3
+ attr_reader :object
4
+ attr_reader :controller
5
+
6
+ def initialize(object, controller = nil)
7
+ @object = object
8
+ @controller = controller
9
+ end
10
+
11
+ def scope
12
+ policy::Scope if policy
13
+ rescue NameError
14
+ nil
15
+ end
16
+
17
+ def policy
18
+ klass = find
19
+ klass = klass.constantize if klass.is_a?(String)
20
+ klass
21
+ rescue NameError
22
+ nil
23
+ end
24
+
25
+ def scope!
26
+ raise NotDefinedError, "unable to find policy scope of nil" if object.nil?
27
+ scope or raise NotDefinedError, "unable to find scope `#{find}::Scope` for `#{object.inspect}`"
28
+ end
29
+
30
+ def policy!
31
+ raise NotDefinedError, "unable to find policy of nil" if object.nil?
32
+ policy or raise NotDefinedError, "unable to find policy `#{find}` for `#{object.inspect}`"
33
+ end
34
+
35
+ private
36
+
37
+ def find
38
+ if object.nil?
39
+ nil
40
+ elsif controller.respond_to?(:policy_class)
41
+ controller.policy_class
42
+ elsif controller.class.respond_to?(:policy_class)
43
+ controller.class.policy_class
44
+ elsif object.respond_to?(:policy_class)
45
+ deprecation_warning("Model#policy_class", "User Controller#policy_class instead.")
46
+ object.policy_class
47
+ elsif object.class.respond_to?(:policy_class)
48
+ deprecation_warning("Model.policy_class", "User Controller.policy_class instead.")
49
+ object.class.policy_class
50
+ else
51
+ klass = if object.respond_to?(:model_name)
52
+ object.model_name
53
+ elsif object.class.respond_to?(:model_name)
54
+ object.class.model_name
55
+ elsif object.is_a?(Class)
56
+ object
57
+ elsif object.is_a?(Symbol)
58
+ object.to_s.camelize
59
+ elsif object.is_a?(Array)
60
+ object.join('/').camelize
61
+ else
62
+ object.class
63
+ end
64
+
65
+ policy_name = "#{klass}#{SUFFIX}"
66
+
67
+ if controller
68
+ "#{controller.policy_namespace}::#{policy_name}"
69
+ else
70
+ policy_name
71
+ end
72
+ end
73
+ end
74
+
75
+ def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
76
+ message = "#{deprecated_method_name} is deprecated and will be removed from Regulator | #{message}"
77
+ Kernel.warn message
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,78 @@
1
+ require "active_support/core_ext/array/conversions"
2
+
3
+ module Regulator
4
+ module RSpec
5
+ module Matchers
6
+ extend ::RSpec::Matchers::DSL
7
+
8
+ matcher :permit do |user, record|
9
+ match_proc = lambda do |policy|
10
+ @violating_permissions = permissions.find_all { |permission| not policy.new(user, record).public_send(permission) }
11
+ @violating_permissions.empty?
12
+ end
13
+
14
+ match_when_negated_proc = lambda do |policy|
15
+ @violating_permissions = permissions.find_all { |permission| policy.new(user, record).public_send(permission) }
16
+ @violating_permissions.empty?
17
+ end
18
+
19
+ failure_message_proc = lambda do |policy|
20
+ was_were = @violating_permissions.count > 1 ? "were" : "was"
21
+ "Expected #{policy} to grant #{permissions.to_sentence} on #{record} but #{@violating_permissions.to_sentence} #{was_were} not granted"
22
+ end
23
+
24
+ failure_message_when_negated_proc = lambda do |policy|
25
+ was_were = @violating_permissions.count > 1 ? "were" : "was"
26
+ "Expected #{policy} not to grant #{permissions.to_sentence} on #{record} but #{@violating_permissions.to_sentence} #{was_were} granted"
27
+ end
28
+
29
+ if respond_to?(:match_when_negated)
30
+ match(&match_proc)
31
+ match_when_negated(&match_when_negated_proc)
32
+ failure_message(&failure_message_proc)
33
+ failure_message_when_negated(&failure_message_when_negated_proc)
34
+ else
35
+ match_for_should(&match_proc)
36
+ match_for_should_not(&match_when_negated_proc)
37
+ failure_message_for_should(&failure_message_proc)
38
+ failure_message_for_should_not(&failure_message_when_negated_proc)
39
+ end
40
+
41
+ def permissions
42
+ current_example = ::RSpec.respond_to?(:current_example) ? ::RSpec.current_example : example
43
+ current_example.metadata[:permissions]
44
+ end
45
+ end
46
+ end
47
+
48
+ module DSL
49
+ def permissions(*list, &block)
50
+ describe(list.to_sentence, :permissions => list, :caller => caller) { instance_eval(&block) }
51
+ end
52
+ end
53
+
54
+ module PolicyExampleGroup
55
+ include Regulator::RSpec::Matchers
56
+
57
+ def self.included(base)
58
+ base.metadata[:type] = :policy
59
+ base.extend Regulator::RSpec::DSL
60
+ super
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ RSpec.configure do |config|
67
+ if RSpec::Core::Version::STRING.split(".").first.to_i >= 3
68
+ config.include(Regulator::RSpec::PolicyExampleGroup, {
69
+ :type => :policy,
70
+ :file_path => /spec\/policies/,
71
+ })
72
+ else
73
+ config.include(Regulator::RSpec::PolicyExampleGroup, {
74
+ :type => :policy,
75
+ :example_group => { :file_path => /spec\/policies/ }
76
+ })
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module Regulator
2
+ VERSION = "0.1.0"
3
+ end
data/lib/regulator.rb ADDED
@@ -0,0 +1,172 @@
1
+ require "regulator/version"
2
+ require "regulator/policy_finder"
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/string/inflections"
5
+ require "active_support/core_ext/object/blank"
6
+ require "active_support/core_ext/module/introspection"
7
+ require "active_support/dependencies/autoload"
8
+
9
+ module Regulator
10
+ SUFFIX = "Policy"
11
+
12
+ class Error < StandardError; end
13
+ class NotAuthorizedError < Error
14
+ attr_reader :query, :record, :policy
15
+
16
+ def initialize(options = {})
17
+ if options.is_a? String
18
+ message = options
19
+ else
20
+ @query = options[:query]
21
+ @record = options[:record]
22
+ @policy = options[:policy]
23
+
24
+ message = options.fetch(:message) { "not allowed to #{query} this #{record.inspect}" }
25
+ end
26
+
27
+ super(message)
28
+ end
29
+ end
30
+ class AuthorizationNotPerformedError < Error; end
31
+ class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end
32
+ class NotDefinedError < Error; end
33
+
34
+ extend ActiveSupport::Concern
35
+
36
+ class << self
37
+ def authorize(user, record, query)
38
+ policy = policy!(user, record)
39
+
40
+ unless policy.public_send(query)
41
+ raise NotAuthorizedError.new(query: query, record: record, policy: policy)
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ def policy_scope(user, scope, controller = nil)
48
+ policy_scope = PolicyFinder.new(scope,controller).scope
49
+ policy_scope.new(user, scope).resolve if policy_scope
50
+ end
51
+
52
+ def policy_scope!(user, scope, controller = nil)
53
+ PolicyFinder.new(scope,controller).scope!.new(user, scope).resolve
54
+ end
55
+
56
+ def policy(user, record, controller = nil)
57
+ policy = PolicyFinder.new(record,controller).policy
58
+ policy.new(user, record) if policy
59
+ end
60
+
61
+ def policy!(user, record, controller = nil)
62
+ PolicyFinder.new(record,controller).policy!.new(user, record)
63
+ end
64
+ end
65
+
66
+ module Helper
67
+ def policy_scope(scope)
68
+ regulator_policy_scope(scope)
69
+ end
70
+ end
71
+
72
+ included do
73
+ def self.policy_namespace
74
+ ( self.parent != Object ? self.parent : nil )
75
+ end
76
+
77
+ def policy_namespace
78
+ @_policy_namespace ||= self.class.policy_namespace
79
+ end
80
+
81
+ helper Helper if respond_to?(:helper)
82
+ if respond_to?(:helper_method)
83
+ helper_method :policy
84
+ helper_method :regulator_policy_scope
85
+ helper_method :regulator_user
86
+ end
87
+ if respond_to?(:hide_action)
88
+ hide_action :policy_namespace
89
+ hide_action :policy
90
+ hide_action :policy_scope
91
+ hide_action :policies
92
+ hide_action :policy_scopes
93
+ hide_action :authorize
94
+ hide_action :verify_authorized
95
+ hide_action :verify_policy_scoped
96
+ hide_action :permitted_attributes
97
+ hide_action :regulator_user
98
+ hide_action :skip_authorization
99
+ hide_action :skip_policy_scope
100
+ hide_action :regulator_policy_authorized?
101
+ hide_action :regulator_policy_scoped?
102
+ end
103
+ end
104
+
105
+ def regulator_policy_authorized?
106
+ !!@_regulator_policy_authorized
107
+ end
108
+
109
+ def regulator_policy_scoped?
110
+ !!@_regulator_policy_scoped
111
+ end
112
+
113
+ def verify_authorized
114
+ raise AuthorizationNotPerformedError unless regulator_policy_authorized?
115
+ end
116
+
117
+ def verify_policy_scoped
118
+ raise PolicyScopingNotPerformedError unless regulator_policy_scoped?
119
+ end
120
+
121
+ def authorize(record, query=nil)
122
+ query ||= params[:action].to_s + "?"
123
+
124
+ @_regulator_policy_authorized = true
125
+
126
+ policy = policy(record)
127
+ unless policy.public_send(query)
128
+ raise NotAuthorizedError.new(query: query, record: record, policy: policy)
129
+ end
130
+
131
+ true
132
+ end
133
+
134
+ def skip_authorization
135
+ @_regulator_policy_authorized = true
136
+ end
137
+
138
+ def skip_policy_scope
139
+ @_regulator_policy_scoped = true
140
+ end
141
+
142
+ def policy_scope(scope)
143
+ @_regulator_policy_scoped = true
144
+ regulator_policy_scope(scope)
145
+ end
146
+
147
+ def policy(record)
148
+ policies[record] ||= Regulator.policy!(regulator_user, record, self)
149
+ end
150
+
151
+ def permitted_attributes(record)
152
+ name = record.class.to_s.demodulize.underscore
153
+ params.require(name).permit(*policy(record).permitted_attributes)
154
+ end
155
+
156
+ def policies
157
+ @_regulator_policies ||= {}
158
+ end
159
+
160
+ def policy_scopes
161
+ @_regulator_policy_scopes ||= {}
162
+ end
163
+
164
+ def regulator_user
165
+ current_user
166
+ end
167
+ private
168
+
169
+ def regulator_policy_scope(scope)
170
+ policy_scopes[scope] ||= Regulator.policy_scope!(regulator_user, scope, self)
171
+ end
172
+ end
data/regulator.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'regulator/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "regulator"
8
+ spec.version = Regulator::VERSION
9
+ spec.authors = ["Cory O'Daniel"]
10
+ spec.email = ["gems@coryodaniel.com"]
11
+
12
+ spec.summary = %q{Minimal controller-based authorization for Rails}
13
+ spec.description = %q{Minimal controller-based authorization for Rails}
14
+ spec.homepage = "https://github.com/coryodaniel/regulator"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "activesupport", ">= 3.0.0"
23
+ spec.add_development_dependency "activemodel", ">= 3.0.0"
24
+ spec.add_development_dependency "actionpack", ">= 3.0.0"
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rspec", ">=2.0.0"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "yard"
30
+ end