chronos_authz 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dc17a6fb18f79cbc7f42761ead6902e96c09d3b3
4
+ data.tar.gz: 57e17def29d8341147bb81ef427055384914e1f2
5
+ SHA512:
6
+ metadata.gz: 53887e05e23adf136dff4856aa8bccec32815e36fde7add0012456db29b1a663a6e56c283f61d64298d634d7be42d4ab05a51530dcd795fda839a842ff91df1b
7
+ data.tar.gz: 92ccdfc2374509a8fd33132c7dbff4f188925b891f7143c8322e3d179f4660eba2d2336e06f6627b81fd718f198c3a6c66a3d3ce92d50089f4e07c47fdb1d4f9
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,104 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ chronos-authz (0.0.1)
5
+ activesupport
6
+ railties (>= 4.2)
7
+ request_store
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actionpack (5.2.0)
13
+ actionview (= 5.2.0)
14
+ activesupport (= 5.2.0)
15
+ rack (~> 2.0)
16
+ rack-test (>= 0.6.3)
17
+ rails-dom-testing (~> 2.0)
18
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
19
+ actionview (5.2.0)
20
+ activesupport (= 5.2.0)
21
+ builder (~> 3.1)
22
+ erubi (~> 1.4)
23
+ rails-dom-testing (~> 2.0)
24
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
25
+ activesupport (5.2.0)
26
+ concurrent-ruby (~> 1.0, >= 1.0.2)
27
+ i18n (>= 0.7, < 2)
28
+ minitest (~> 5.1)
29
+ tzinfo (~> 1.1)
30
+ ansi (1.5.0)
31
+ builder (3.2.3)
32
+ concurrent-ruby (1.0.5)
33
+ crass (1.0.4)
34
+ diff-lcs (1.3)
35
+ docile (1.3.1)
36
+ erubi (1.7.1)
37
+ hirb (0.7.3)
38
+ i18n (1.0.1)
39
+ concurrent-ruby (~> 1.0)
40
+ json (2.1.0)
41
+ loofah (2.2.2)
42
+ crass (~> 1.0.2)
43
+ nokogiri (>= 1.5.9)
44
+ method_source (0.9.0)
45
+ mini_portile2 (2.3.0)
46
+ minitest (5.11.3)
47
+ nokogiri (1.8.4)
48
+ mini_portile2 (~> 2.3.0)
49
+ rack (2.0.5)
50
+ rack-test (1.1.0)
51
+ rack (>= 1.0, < 3)
52
+ rails-dom-testing (2.0.3)
53
+ activesupport (>= 4.2.0)
54
+ nokogiri (>= 1.6)
55
+ rails-html-sanitizer (1.0.4)
56
+ loofah (~> 2.2, >= 2.2.2)
57
+ railties (5.2.0)
58
+ actionpack (= 5.2.0)
59
+ activesupport (= 5.2.0)
60
+ method_source
61
+ rake (>= 0.8.7)
62
+ thor (>= 0.18.1, < 2.0)
63
+ rake (12.0.0)
64
+ request_store (1.4.1)
65
+ rack (>= 1.4)
66
+ rspec (3.7.0)
67
+ rspec-core (~> 3.7.0)
68
+ rspec-expectations (~> 3.7.0)
69
+ rspec-mocks (~> 3.7.0)
70
+ rspec-core (3.7.1)
71
+ rspec-support (~> 3.7.0)
72
+ rspec-expectations (3.7.0)
73
+ diff-lcs (>= 1.2.0, < 2.0)
74
+ rspec-support (~> 3.7.0)
75
+ rspec-mocks (3.7.0)
76
+ diff-lcs (>= 1.2.0, < 2.0)
77
+ rspec-support (~> 3.7.0)
78
+ rspec-support (3.7.1)
79
+ simplecov (0.16.1)
80
+ docile (~> 1.1)
81
+ json (>= 1.8, < 3)
82
+ simplecov-html (~> 0.10.0)
83
+ simplecov-console (0.4.2)
84
+ ansi
85
+ hirb
86
+ simplecov
87
+ simplecov-html (0.10.2)
88
+ thor (0.20.0)
89
+ thread_safe (0.3.6)
90
+ tzinfo (1.2.5)
91
+ thread_safe (~> 0.1)
92
+
93
+ PLATFORMS
94
+ ruby
95
+
96
+ DEPENDENCIES
97
+ chronos-authz!
98
+ rake (>= 11.3.0)
99
+ rspec
100
+ simplecov
101
+ simplecov-console
102
+
103
+ BUNDLED WITH
104
+ 1.16.2
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # chronos_authz
2
+ A declarative authorization Rack middleware that supports custom authorization logic on a per-resource basis
3
+
4
+ ## Usage sample for Rails
5
+ ### 1. Install the gem
6
+ ```ruby
7
+ gem 'chronos_authz'
8
+ ```
9
+
10
+ ### 2. Configure the ACL config/my_acl.yml
11
+ An incoming request's http method and path will be checked against the ACL file's records for a match. At minimum, you MUST configure the path of the resource in an ACL record and SHOULD configure the http_method. If the http_method isn't configured, http method checking will not be done. You can define any other configuration here per resource as needed (ex. :permissions is a custom configuration and is defined here as this will be used in the custom authorization rule.
12
+
13
+ ```ruby
14
+ manage_accounts:
15
+ path: "/accounts/.*" # regex pattern
16
+ http_method: # if array is used, this would work as an OR operation: checking if the incoming http request's method matches ANY of the configured http_method
17
+ - GET
18
+ - put
19
+ permissions:
20
+ - VIEW_ACCOUNTS
21
+ - UPDATE_ACCOUNTS
22
+
23
+ create_users:
24
+ path: "/users"
25
+ http_method: POST
26
+ permissions:
27
+ - CREATE_USERS
28
+
29
+ update_users:
30
+ path: "/users/.*"
31
+ http_method: PUT
32
+ permissions:
33
+ - UPDATE_USERS
34
+ # rule: AnotherCustomRule # Use a different rule for this ACL record
35
+ ```
36
+
37
+ ### 2. Create an authorization rule initializers/MyCustomRule.rb
38
+ An authorization rule MUST implement the __request_authorized?__ method. The rule has an access to the ff. instance variables:
39
+
40
+ 1. @request - [__Rack::Request__](https://www.rubydoc.info/gems/rack/Rack/Request) for the incoming HTTP request
41
+ 2. @acl_record - __ChronosAuthz::ACL::Record__ from the ACL yml that matches the incoming request's http method and path. Custom configuration in the ACL yaml will be accessible from this object. ex. @acl_record.permissions
42
+
43
+ ```ruby
44
+ class MyCustomRule < ChronosAuthz::Rule
45
+
46
+ # Must return a boolean to check
47
+ def request_authorized?
48
+ (@acl_record.permissions - user_claims).blank?
49
+ end
50
+
51
+ # Optional. Implement how claims are retrieved for a given user. Normally claims could be retrieved using cookies,
52
+ # JWT/id_token decoded from the request header, API calls, or a database query. Any value returned here will be available to the ChronosAuthz::User.claims helper module as well.
53
+ def user_claims
54
+ # SAMPLE ONLY! In this sample configuration, only the user with the access token '1d1234913em23' would only be able to successfully send a POST request to /users. Access token 'm123493429304' bearer could both create and update a User.
55
+ access_tokens = {
56
+ "1d1234913em23" => ["CREATE_USERS"],
57
+ "m123493429304" => ["CREATE_USERS", "UPDATE_USERS", "SomeOtherClaimInOtherFormat", "any-format-should-work-claim"]
58
+ }
59
+
60
+ access_token_from_request = @request.get_header("HTTP_AUTHORIZATION").gsub("Bearer ")
61
+ return (access_tokens[access_token_from_request] || [])
62
+ end
63
+ end
64
+ ```
65
+
66
+ ### 4. Use the middleware
67
+ ```ruby
68
+ Rails.application.config.middleware.use ChronosAuthz::Authorizer do |config|
69
+ # Required. Default authorization rule to use
70
+ config.default_rule = MyCustomRule
71
+
72
+ # Optional. Default is false. If set to true, the ACL is treated as a whitelist of resource paths: authorization would return a 403 error if no ACL Record has been configured for a given resource path. If set to false, authorization check will only be done to the resources configured in the ACL.
73
+ config.strict_mode = true
74
+
75
+ # Optional. Configure the error page to render when authorization fails
76
+ config.error_page = "public/403.html"
77
+
78
+ # Optional. Default behavior will look for 'config/authorizer_acl.yml'. Configure which ACL yml to use
79
+ config.acl_yaml = "config/my_acl.yml"
80
+ end
81
+ ```
82
+
83
+ ## Helpers
84
+ Include the helper module __ChronosAuthz::User__ as needed to have access to the current user's claims via the __.claims__ method.
85
+ example:
86
+ ```ruby
87
+ class User < ActiveRecord
88
+ include ChronosAuthz::User
89
+ end
90
+ ```
91
+
92
+ With this helper included in your User model and assuming you are using Devise or any other AuthN solution, you may do the ff.:
93
+ ```ruby
94
+ current_user.claims
95
+ => ["CREATE_USERS"]
96
+ ```
97
+
98
+ If the return value of user_claims method in your implementation is a hash:
99
+ ```ruby
100
+ current_user.claims
101
+ => {permissions: ["CREATE_USERS"], email: "someemail@yourdomain.com"}
102
+
103
+ current_user.claim[:email]
104
+ => someemail@yourdomain.com
105
+ ```
106
+
107
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+
3
+ require "chronos_authz/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "chronos_authz"
7
+ s.version = ChronosAuthz::VERSION
8
+ s.authors = ["Marianne Angelie del Mundo", "Rodette Pedro", "JR Respino", "Jayson Uy"]
9
+ s.email = %w(marianne@chronoscloud.com rodette@chronoscloud.com jr@chronoscloud.com jayson@chronoscloud.com)
10
+ s.homepage = "https://github.com/chronoscloud/chronoscloud-authz"
11
+ s.summary = "A minimal and declarative authorization layer"
12
+ s.description = "A declarative authorization Rack middleware that supports custom authorization logic on a per-resource basis"
13
+ s.license = 'N/A'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- spec/*`.split("\n")
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_dependency "railties", ">= 4.2"
20
+ s.add_dependency "request_store"
21
+ s.add_dependency "activesupport"
22
+ s.required_ruby_version = ">= 2.4"
23
+
24
+ s.add_development_dependency "rake", ">= 11.3.0"
25
+ s.add_development_dependency "rspec-rails"
26
+ s.add_development_dependency "simplecov"
27
+ s.add_development_dependency "simplecov-console"
28
+ end
@@ -0,0 +1,16 @@
1
+ require 'active_support/core_ext/string'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+ require 'ostruct'
4
+ require 'request_store'
5
+ require 'yaml'
6
+
7
+ require 'chronos_authz/validations/validation_error'
8
+ require 'chronos_authz/validations/options_validator'
9
+ require 'chronos_authz/configuration'
10
+ require 'chronos_authz/acl'
11
+ require 'chronos_authz/rule'
12
+ require 'chronos_authz/user'
13
+ require 'chronos_authz/authorizer'
14
+
15
+ module ChronosAuthz
16
+ end
@@ -0,0 +1,81 @@
1
+ module ChronosAuthz
2
+ class ACL
3
+ attr_accessor :acl_hash, :records
4
+
5
+ def self.build_from_yaml(acl_yaml = nil)
6
+ acl_yaml ||= 'config/authorizer_acl.yml'
7
+ acl_hash = YAML.load_file(acl_yaml)
8
+ records = ACL.records_from_acl_hash(acl_hash)
9
+ return ACL.new(records, acl_hash)
10
+ end
11
+
12
+ # Populate @records with instances of ACL::Record and validate each instances
13
+ def self.records_from_acl_hash(acl_hash)
14
+ return acl_hash.map do |record_name, record_options = {}|
15
+ ChronosAuthz::ACL::Record.new(record_options.merge!(name: record_name))
16
+ end
17
+ end
18
+
19
+ def initialize(records = [], acl_hash = nil)
20
+ @records = records
21
+ @acl_hash = acl_hash
22
+ end
23
+
24
+ # Find matching ACL Record
25
+ def find_match(http_method, request_path)
26
+ record = @records.select{ |record| record.matches?(http_method, request_path) }.first
27
+ # puts "[ChronosAuthz] Found ACL match: #{record.to_s}" if record
28
+
29
+ return record
30
+ end
31
+
32
+
33
+ class Record < OpenStruct
34
+ include ChronosAuthz::Validations::OptionsValidator
35
+
36
+ VALID_HTTP_METHODS = ["GET",
37
+ "POST",
38
+ "PUT",
39
+ "PATCH",
40
+ "DELETE",
41
+ "HEAD"].freeze
42
+
43
+ required :name, :path
44
+ check_constraint :http_method, VALID_HTTP_METHODS, allow_nil: true
45
+
46
+ def initialize(value = {})
47
+ value = value.with_indifferent_access
48
+ value[:http_method] = normalize_http_methods(value[:http_method])
49
+ value[:path] = normalize_path(value[:path])
50
+
51
+ super(value)
52
+ validate!
53
+ end
54
+
55
+ def matches?(http_method, request_path)
56
+ return false if request_path.nil?
57
+
58
+ request_path = normalize_path(request_path)
59
+ path_pattern = /\A#{self.path}\z/
60
+
61
+ method_matched = self.http_method.empty? || self.http_method.include?(http_method.to_s.upcase)
62
+
63
+ return false if !method_matched
64
+ return !request_path.match(path_pattern).nil?
65
+ end
66
+
67
+ private
68
+
69
+ def normalize_path(resource_path)
70
+ return resource_path.squish if !resource_path.nil?
71
+ end
72
+
73
+ def normalize_http_methods(http_methods)
74
+ http_methods = [http_methods] if !http_methods.is_a? Array
75
+ http_methods.map!{ |http_method| http_method.to_s.upcase }
76
+
77
+ return http_methods.reject { |http_method| http_method.blank? }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,39 @@
1
+ module ChronosAuthz
2
+
3
+ class Authorizer
4
+
5
+ attr_accessor :configuration, :acl
6
+
7
+ def initialize(app, options = {})
8
+ @app, @configuration = app, ::ChronosAuthz::Configuration.new(options)
9
+
10
+ yield @configuration if block_given?
11
+ @configuration.validate!
12
+ @acl = ChronosAuthz::ACL.build_from_yaml(@configuration.acl_yaml)
13
+ end
14
+
15
+
16
+ def call(env)
17
+ matched_acl_record = @acl.find_match(env["REQUEST_METHOD"], env["PATH_INFO"])
18
+
19
+ return render_unauthorized if @configuration.strict_mode && matched_acl_record.nil?
20
+
21
+ request = Rack::Request.new(env)
22
+ rule_class = matched_acl_record.try(:rule).try(:constantize) || @configuration.default_rule
23
+ rule_instance = rule_class.new(request, matched_acl_record)
24
+
25
+ return render_unauthorized if !rule_instance.request_authorized?
26
+
27
+ RequestStore.store[:chronos_authz_claims] = rule_instance.user_claims
28
+ status, headers, response = @app.call(env)
29
+ end
30
+
31
+ def render_unauthorized
32
+ if @configuration.error_page
33
+ # html = ActionView::Base.new.render(file: @configuration.error_page)
34
+ return [403, {'Content-Type' => 'text/html'}, [File.read(@configuration.error_page)]]
35
+ end
36
+ return [403, {'Content-Type' => 'text/plain'}, ["Unauthorized"]]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ module ChronosAuthz
2
+ class Configuration < OpenStruct
3
+ include ChronosAuthz::Validations::OptionsValidator
4
+
5
+ required :default_rule
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ module ChronosAuthz
2
+ class Rule
3
+
4
+ attr_accessor :request, :acl_record
5
+
6
+ def initialize(request, acl_record)
7
+ @request = request
8
+ @acl_record = acl_record
9
+ end
10
+
11
+ def user_claims
12
+ nil
13
+ end
14
+
15
+ def request_authorized?
16
+ false
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module ChronosAuthz
2
+ module User
3
+
4
+ def claim(key)
5
+ claims[key]
6
+ end
7
+
8
+ def claims
9
+ RequestStore.store[:chronos_authz_claims]
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,60 @@
1
+ module ChronosAuthz
2
+ module Validations
3
+ module OptionsValidator
4
+
5
+ def self.included base
6
+ base.extend OptionsValidatorClassMethods
7
+ end
8
+
9
+ module OptionsValidatorClassMethods
10
+ attr_accessor :required_options, :predefined_value_map
11
+
12
+ def required(*options)
13
+ self.required_options = options
14
+ end
15
+
16
+ def check_constraint(option, predefined_values = [], constraint_options = {})
17
+ self.predefined_value_map ||= {}
18
+ self.predefined_value_map[option] = { check_values: predefined_values,
19
+ constraint_options: constraint_options }
20
+ end
21
+ end
22
+
23
+ def validate!
24
+
25
+ # Required options
26
+ if !self.class.required_options.nil?
27
+ self.class.required_options.each do |required_option|
28
+ option_value = send(required_option) if respond_to?(required_option)
29
+ raise ChronosAuthz::Validations::ValidationError.new("Missing option #{required_option} in #{self.class}") if option_value.blank?
30
+ end
31
+ end
32
+
33
+ # Check constraint
34
+ if !self.class.predefined_value_map.nil?
35
+ self.class.predefined_value_map.each do |key, value|
36
+ option_values = send(key)
37
+ check_values = value[:check_values]
38
+
39
+ if !option_values.is_a? Array
40
+ option_values = [option_values]
41
+ end
42
+
43
+ if value[:constraint_options][:case_sensitive].nil? || !value[:constraint_options][:case_sensitive]
44
+ option_values = option_values.map{|option_value| option_value.to_s.upcase }
45
+ check_values = check_values.map{|check_value| check_value.to_s.upcase }
46
+ end
47
+
48
+ option_values.each do |option_value|
49
+ next if value[:constraint_options][:allow_nil] && (option_value.nil? || option_value.empty?)
50
+ raise ChronosAuthz::Validations::ValidationError.new("Invalid option value #{option_value} for #{key} in #{self.class}. Valid values are #{check_values}.") if !check_values.include?(option_value)
51
+ end
52
+ end
53
+ end
54
+
55
+ self
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,11 @@
1
+ module ChronosAuthz
2
+ module Validations
3
+ class ValidationError < StandardError
4
+
5
+ def initialize(message)
6
+ super(message)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module ChronosAuthz
2
+ VERSION = "0.0.1"
3
+ end
data/spec/acl_spec.rb ADDED
@@ -0,0 +1,186 @@
1
+ require 'spec_helper'
2
+
3
+ describe ChronosAuthz::ACL do
4
+ let(:acl_yaml) { 'spec/config/authorizer_acl_test.yml' }
5
+
6
+ describe 'attribute accessors' do
7
+ it 'assigns acl_hash' do
8
+ acl_object = ChronosAuthz::ACL.build_from_yaml(acl_yaml)
9
+
10
+ expect(acl_object.acl_hash).to_not be_nil
11
+ end
12
+
13
+ it 'assigns records' do
14
+ acl_object = ChronosAuthz::ACL.build_from_yaml(acl_yaml)
15
+
16
+ expect(acl_object.records).to_not be_empty
17
+ end
18
+ end
19
+
20
+ describe '.build_from_yaml' do
21
+ context 'when acl_yaml is specified' do
22
+ it 'loads a YAML file from the acl_yaml' do
23
+ allow(YAML).to receive(:load_file).and_return(YAML.load_file(acl_yaml))
24
+ expect(YAML).to receive(:load_file).with(acl_yaml)
25
+
26
+ ChronosAuthz::ACL.build_from_yaml(acl_yaml)
27
+ end
28
+ end
29
+
30
+ context 'when no acl_yaml is specified' do
31
+ it 'loads a YAML file using a default path' do
32
+ allow(YAML).to receive(:load_file).and_return(YAML.load_file(acl_yaml))
33
+ expect(YAML).to receive(:load_file).with('config/authorizer_acl.yml')
34
+
35
+ ChronosAuthz::ACL.build_from_yaml
36
+ end
37
+ end
38
+
39
+ it 'initializes a new ACL' do
40
+ expect(ChronosAuthz::ACL).to receive(:new)
41
+
42
+ ChronosAuthz::ACL.build_from_yaml(acl_yaml)
43
+ end
44
+
45
+ it 'returns an ACL object' do
46
+ expect(ChronosAuthz::ACL.build_from_yaml(acl_yaml)).to be_an_instance_of(ChronosAuthz::ACL)
47
+ end
48
+
49
+ end
50
+
51
+ describe '.records_from_acl_hash' do
52
+ it 'returns an array of ACL::Record from a YAML hash' do
53
+ records = ChronosAuthz::ACL.records_from_acl_hash(YAML.load_file(acl_yaml))
54
+ expect(records).to_not be_empty
55
+ expect(records).to all(be_an(ChronosAuthz::ACL::Record))
56
+ end
57
+ end
58
+
59
+ describe '#find_match' do
60
+ let(:acl) { ChronosAuthz::ACL.build_from_yaml(acl_yaml) }
61
+
62
+ context 'when using valid parameters' do
63
+ context 'when http_method and request_path is in config' do
64
+
65
+ it 'returns an ACLRecord' do
66
+ expect(acl.find_match('POST', '/users')).to be_an_instance_of(ChronosAuthz::ACL::Record)
67
+ end
68
+
69
+ it 'finds the record that matches the path pattern' do
70
+ expect(acl.find_match('GET', '/accounts/2')).to_not be_nil
71
+ end
72
+ end
73
+
74
+ context 'when http_method is not in config' do
75
+ it 'returns nil' do
76
+ expect(acl.find_match('DELETE', '/users')).to be_nil
77
+ end
78
+ end
79
+
80
+ context 'when request_path is not in config' do
81
+ it 'returns nil' do
82
+ expect(acl.find_match('GET', '/test')).to be_nil
83
+ expect(acl.find_match('POST', '/users////')).to be_nil
84
+ expect(acl.find_match('POST', '/user/1/1/1/')).to be_nil
85
+ end
86
+ end
87
+ end
88
+
89
+ context 'when using null parameters' do
90
+ context 'when http_method is null' do
91
+ it 'returns nil' do
92
+ expect(acl.find_match(nil, '/test')).to be_nil
93
+ end
94
+ end
95
+
96
+ context 'when resource_path is null' do
97
+ it 'returns nil' do
98
+ expect(acl.find_match('GET', 'nil')).to be_nil
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+
106
+ describe ChronosAuthz::ACL::Record do
107
+ let(:acl_record_string_method) { ChronosAuthz::ACL::Record.new(name: 'create_users',
108
+ http_method: 'post',
109
+ path: ' /users ') }
110
+ let(:acl_record_array_method) { ChronosAuthz::ACL::Record.new(name: 'create_users',
111
+ http_method: ['POST', 'put'],
112
+ path: '/users') }
113
+ let(:acl_record_implicit_all_method) { ChronosAuthz::ACL::Record.new(name: 'create_users',
114
+ path: '/users') }
115
+
116
+ describe 'initialization' do
117
+ context 'when http_method is specified' do
118
+ context 'with a valid HTTP method' do
119
+ it 'assigns http_method as a normalized array' do
120
+ expect(acl_record_string_method.http_method).to_not be_empty
121
+ expect(acl_record_string_method.http_method).eql?(['POST'])
122
+ end
123
+ end
124
+
125
+ context 'with an invalid HTTP method' do
126
+ it 'raises a validation error' do
127
+ expect{ ChronosAuthz::ACL::Record.new(name: 'create_users',
128
+ http_method: 'GETS',
129
+ path: '/users') }.to raise_error(ChronosAuthz::Validations::ValidationError)
130
+ end
131
+ end
132
+ end
133
+
134
+ context 'when path is specified' do
135
+ it 'assigns a squished http_method' do
136
+ expect(acl_record_string_method.path).eql?("/users")
137
+ end
138
+ end
139
+
140
+ it 'raises a validation error if name is not specified' do
141
+ expect{ ChronosAuthz::ACL::Record.new(path: '/users') }.to raise_error(ChronosAuthz::Validations::ValidationError)
142
+ end
143
+
144
+ it 'raises a validation error if path is not specified' do
145
+ expect{ ChronosAuthz::ACL::Record.new(name: 'create_users') }.to raise_error(ChronosAuthz::Validations::ValidationError)
146
+ end
147
+
148
+ end
149
+
150
+ describe '#matches?' do
151
+ context 'when request_path matches the request_path regex pattern config' do
152
+ context 'with an array http_method config' do
153
+ it 'returns true if http_method has a match from the http_method array config' do
154
+ expect(acl_record_array_method.matches?('POST', '/users')).to be true
155
+ end
156
+
157
+ it 'returns false if http_method doesn\'t have a match from the http_method array config' do
158
+ expect(acl_record_array_method.matches?('GET', '/accounts')).to be false
159
+ end
160
+ end
161
+
162
+ context 'with a string http_method config' do
163
+ it 'returns true if http_method matches the http_method string config' do
164
+ expect(acl_record_string_method.matches?('POST', '/users')).to be true
165
+ end
166
+
167
+ it 'returns false if http_method doesn\'t match the http_method string config' do
168
+ expect(acl_record_string_method.matches?('get', '/accounts')).to be false
169
+ end
170
+ end
171
+
172
+ context 'with a nil http_method config' do
173
+ it 'returns true' do
174
+ expect(acl_record_implicit_all_method.matches?('post', '/users')).to be true
175
+ end
176
+ end
177
+ end
178
+
179
+ context 'when request_path doesn\'t match the request_path regex pattern config' do
180
+ it 'returns false' do
181
+ expect(acl_record_implicit_all_method.matches?('post', '/account/')).to be false
182
+ end
183
+ end
184
+ end
185
+
186
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+ require 'rack'
3
+ describe ChronosAuthz::Authorizer do
4
+
5
+ class AuthorizeAllRequestRule < ChronosAuthz::Rule
6
+ def request_authorized?
7
+ true
8
+ end
9
+ end
10
+
11
+ class BlockAllRequestsRule < ChronosAuthz::Rule
12
+ def request_authorized?
13
+ false
14
+ end
15
+ end
16
+
17
+ describe "Middleware configuration" do
18
+
19
+ it "should raise an error if default_rule isn't configured" do
20
+ env = mock_env("/")
21
+ app = lambda{}
22
+ expect{ rack_app(app).call(env) }.to raise_error(ChronosAuthz::Validations::ValidationError)
23
+ end
24
+
25
+ it "should use default_rule for authorization check when no :rule option was specified in the matched ACL record" do
26
+ env = mock_env("/accounts/")
27
+ app = lambda{ |env| [200, {'Content-Type' => 'text/plain'}, ["OK"]] }
28
+ default_rule_class = AuthorizeAllRequestRule
29
+ expect_any_instance_of(default_rule_class).to receive(:request_authorized?)
30
+ rack_app(app, :default_rule => default_rule_class, :strict_mode => false).call(env)
31
+ end
32
+
33
+ it "should use the matched ACL Record's :rule option if it is specified" do
34
+ env = mock_env("/users", method: "POST")
35
+ app = lambda{ |env| [200, {'Content-Type' => 'text/plain'}, ["OK"]] }
36
+ expect_any_instance_of(ChronosAuthz::Spec::Helpers::CustomRule).to receive(:request_authorized?)
37
+ expect_any_instance_of(AuthorizeAllRequestRule).to_not receive(:request_authorized?)
38
+ rack_app(app, :default_rule => AuthorizeAllRequestRule, :strict_mode => false).call(env)
39
+ end
40
+
41
+ it "should use the configured error_page if the request is unauthorized" do
42
+ env = mock_env("/accounts/123", method: "PUT")
43
+ app = lambda{ |env| [200, {'Content-Type' => 'text/plain'}, ["OK"]] }
44
+ error_page_path = 'spec/config/error_page.html'
45
+ error_page_contents = File.read(error_page_path)
46
+ result = rack_app(app, :default_rule => BlockAllRequestsRule, :error_page => error_page_path).call(env)
47
+ expect(result.last.first).to eq(error_page_contents)
48
+ end
49
+ end
50
+
51
+ describe "Authorization" do
52
+
53
+ it "should fail with a 403 response if strict_mode configuration is set to true and no ACL Record was configured for the incoming request" do
54
+ env = mock_env("/some_unknown_path")
55
+ app = lambda{}
56
+ result = rack_app(app, :default_rule => AuthorizeAllRequestRule, :strict_mode => true).call(env)
57
+ expect(result.first).to eq(403)
58
+ end
59
+
60
+ it "should not fail with a 403 response if strict_mode configuration is not set to true and no ACL Record was configured for the incoming request" do
61
+ env = mock_env("/")
62
+ app = lambda{ |env| [200, {'Content-Type' => 'text/plain'}, ["OK"]] }
63
+ result = rack_app(app, :default_rule => AuthorizeAllRequestRule, :strict_mode => false).call(env)
64
+ expect(result.first).to eq(200)
65
+ end
66
+
67
+ it "should fail with a 403 response if authorization check failed" do
68
+ env = mock_env("/")
69
+ app = lambda{}
70
+ result = rack_app(app, :default_rule => BlockAllRequestsRule).call(env)
71
+ expect(result.first).to eq(403)
72
+ end
73
+
74
+ it "should not fail with a 403 response if authorization check succeeded" do
75
+ env = mock_env("/")
76
+ app = lambda{ |env| [200, {'Content-Type' => 'text/plain'}, ["OK"]] }
77
+ result = rack_app(app, :default_rule => AuthorizeAllRequestRule).call(env)
78
+ expect(result.first).to eq(200)
79
+ end
80
+ end
81
+
82
+ def rack_app(app, options= {})
83
+ options[:acl_yaml] ||= 'spec/config/authorizer_acl_test.yml'
84
+ Rack::Builder.new do
85
+ use ChronosAuthz::Authorizer, options
86
+ run app
87
+ end
88
+ end
89
+
90
+ def mock_env(path = "/", params = {})
91
+ method = params.delete(:method) || "GET"
92
+ env = { 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => "#{method}" }
93
+ Rack::MockRequest.env_for("#{path}?#{Rack::Utils.build_query(params)}", env)
94
+ end
95
+
96
+ end
@@ -0,0 +1,18 @@
1
+ manage_accounts:
2
+ path: "/accounts/.*"
3
+ http_method:
4
+ - GET
5
+ - put
6
+ permissions:
7
+ - AUTH::VIEW_ACCOUNT
8
+ - AUTH::MANAGE_ACCOUNTS
9
+
10
+ create_users:
11
+ path: "/users"
12
+ http_method: POST
13
+ permissions:
14
+ - AUTH::CREATE_USERS
15
+ rule: ChronosAuthz::Spec::Helpers::CustomRule
16
+
17
+ shipment:
18
+ path: "/*"
@@ -0,0 +1 @@
1
+ Custom error page
@@ -0,0 +1,9 @@
1
+ module ChronosAuthz::Spec
2
+ module Helpers
3
+ class CustomRule < ChronosAuthz::Rule
4
+ def request_authorized?
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ describe ChronosAuthz::Validations::OptionsValidator do
4
+ PRIMARY_COLORS = ["red", "yellow", "blue"].freeze
5
+
6
+ class WithRequiredAndCheckConstraint < OpenStruct
7
+ include ChronosAuthz::Validations::OptionsValidator
8
+ required :some_required_attribute
9
+ check_constraint :primary_color, PRIMARY_COLORS
10
+ end
11
+
12
+ class WithRequiredCheckConstraintOption < OpenStruct
13
+ include ChronosAuthz::Validations::OptionsValidator
14
+ check_constraint :primary_color, PRIMARY_COLORS, allow_nil: false
15
+ end
16
+
17
+ class WithCheckConstraint < OpenStruct
18
+ include ChronosAuthz::Validations::OptionsValidator
19
+ check_constraint :primary_color, ["red", "yellow", "blue"], allow_nil: true
20
+ end
21
+
22
+ class WithCaseSensitiveCheckConstraintOption < OpenStruct
23
+ include ChronosAuthz::Validations::OptionsValidator
24
+ check_constraint :primary_color, ["red", "yellow", "blue"], case_sensitive: true
25
+ end
26
+
27
+ describe 'required options validation' do
28
+
29
+ it 'raises a validation error if a required option is nil' do
30
+ expect{ WithRequiredAndCheckConstraint.new.validate! }.to raise_error(ChronosAuthz::Validations::ValidationError)
31
+ end
32
+
33
+ end
34
+
35
+ describe 'check constraint validation' do
36
+
37
+ context 'when option value is a string' do
38
+ it 'raises a validation error if the option value isn\'t found from the valid option values' do
39
+ expect{ WithRequiredAndCheckConstraint.new(some_required_attribute: 'some_value', primary_color: "brown").validate! }.to raise_error(ChronosAuthz::Validations::ValidationError)
40
+ end
41
+
42
+ context 'with allow_nil config set to false' do
43
+ it 'raises a validation error if option value is nil' do
44
+ expect{ WithRequiredCheckConstraintOption.new.validate! }.to raise_error(ChronosAuthz::Validations::ValidationError)
45
+ end
46
+ end
47
+
48
+ context 'with allow_nil config set to true' do
49
+ it 'doesn\'t raise any validation errors if option value is nil' do
50
+ expect{ WithCheckConstraint.new.validate! }.to_not raise_error(ChronosAuthz::Validations::ValidationError)
51
+ end
52
+ end
53
+
54
+ context 'with case_sensitive config set to true' do
55
+ it 'raises a validation error if option value is meaningfully equal to any of the valid option values but in different case' do
56
+ expect{ WithCaseSensitiveCheckConstraintOption.new(primary_color: "RED").validate! }.to raise_error(ChronosAuthz::Validations::ValidationError)
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'when option value is an array' do
62
+ it 'raises a validation error if an element from the option value isn\'t found from the valid option values' do
63
+ expect{ WithRequiredAndCheckConstraint.new(some_required_attribute: 'some_value', primary_color: ["RED", "brown"]).validate! }.to raise_error(ChronosAuthz::Validations::ValidationError)
64
+ end
65
+
66
+ context 'with allow_nil config set to false' do
67
+ it 'raises a validation error if an element from the option value is nil' do
68
+ expect{ WithRequiredCheckConstraintOption.new(primary_color: ["blue", nil]).validate! }.to raise_error(ChronosAuthz::Validations::ValidationError)
69
+ end
70
+ end
71
+
72
+ context 'with allow_nil config set to true' do
73
+ it 'doesn\'t raise any validation errors if an element from the option value is nil' do
74
+ expect{ WithCheckConstraint.new(primary_color: ["blue", nil]).validate! }.to_not raise_error(ChronosAuthz::Validations::ValidationError)
75
+ end
76
+ end
77
+
78
+ context 'with case_sensitive config set to true' do
79
+ it 'raises a validation error if an element from the option value is meaningfully equal to any of the valid option values but in different case' do
80
+ expect{ WithCaseSensitiveCheckConstraintOption.new(primary_color: ["blue", "ReD"]).validate! }.to raise_error(ChronosAuthz::Validations::ValidationError)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,115 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'chronos_authz'
5
+
6
+ require 'simplecov'
7
+ require 'simplecov-console'
8
+
9
+ Dir[File.join(File.dirname(__FILE__), "helpers", "**/*.rb")].each do |f|
10
+ require f
11
+ end
12
+
13
+ SimpleCov.formatter = SimpleCov.formatter = SimpleCov::Formatter::Console
14
+ SimpleCov.start
15
+
16
+ # This file was generated by the `rspec --init` command. Conventionally, all
17
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
18
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
19
+ # this file to always be loaded, without a need to explicitly require it in any
20
+ # files.
21
+ #
22
+ # Given that it is always loaded, you are encouraged to keep this file as
23
+ # light-weight as possible. Requiring heavyweight dependencies from this file
24
+ # will add to the boot time of your test suite on EVERY test run, even for an
25
+ # individual file that may not need all of that loaded. Instead, consider making
26
+ # a separate helper file that requires the additional dependencies and performs
27
+ # the additional setup, and require it from the spec files that actually need
28
+ # it.
29
+ #
30
+ # The `.rspec` file also contains a few flags that are not defaults but that
31
+ # users commonly want.
32
+ #
33
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
34
+ RSpec::Expectations.configuration.on_potential_false_positives = :nothing
35
+ RSpec.configure do |config|
36
+ # rspec-expectations config goes here. You can use an alternate
37
+ # assertion/expectation library such as wrong or the stdlib/minitest
38
+ # assertions if you prefer.
39
+ # config.include(ChronosAuthz::Spec::Helpers)
40
+
41
+ config.expect_with :rspec do |expectations|
42
+ # This option will default to `true` in RSpec 4. It makes the `description`
43
+ # and `failure_message` of custom matchers include text for helper methods
44
+ # defined using `chain`, e.g.:
45
+ # be_bigger_than(2).and_smaller_than(4).description
46
+ # # => "be bigger than 2 and smaller than 4"
47
+ # ...rather than:
48
+ # # => "be bigger than 2"
49
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
50
+ end
51
+
52
+
53
+ # rspec-mocks config goes here. You can use an alternate test double
54
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
55
+ config.mock_with :rspec do |mocks|
56
+ # Prevents you from mocking or stubbing a method that does not exist on
57
+ # a real object. This is generally recommended, and will default to
58
+ # `true` in RSpec 4.
59
+ mocks.verify_partial_doubles = true
60
+ end
61
+
62
+ # The settings below are suggested to provide a good initial experience
63
+ # with RSpec, but feel free to customize to your heart's content.
64
+ =begin
65
+ # These two settings work together to allow you to limit a spec run
66
+ # to individual examples or groups you care about by tagging them with
67
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
68
+ # get run.
69
+ config.filter_run :focus
70
+ config.run_all_when_everything_filtered = true
71
+
72
+ # Allows RSpec to persist some state between runs in order to support
73
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
74
+ # you configure your source control system to ignore this file.
75
+ config.example_status_persistence_file_path = "spec/examples.txt"
76
+
77
+ # Limits the available syntax to the non-monkey patched syntax that is
78
+ # recommended. For more details, see:
79
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
80
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
81
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
82
+ config.disable_monkey_patching!
83
+
84
+ # This setting enables warnings. It's recommended, but in some cases may
85
+ # be too noisy due to issues in dependencies.
86
+ config.warnings = true
87
+
88
+ # Many RSpec users commonly either run the entire suite or an individual
89
+ # file, and it's useful to allow more verbose output when running an
90
+ # individual spec file.
91
+ if config.files_to_run.one?
92
+ # Use the documentation formatter for detailed output,
93
+ # unless a formatter has already been configured
94
+ # (e.g. via a command-line flag).
95
+ config.default_formatter = 'doc'
96
+ end
97
+
98
+ # Print the 10 slowest examples and example groups at the
99
+ # end of the spec run, to help surface which specs are running
100
+ # particularly slow.
101
+ config.profile_examples = 10
102
+
103
+ # Run specs in random order to surface order dependencies. If you find an
104
+ # order dependency and want to debug it, you can fix the order by providing
105
+ # the seed, which is printed after each run.
106
+ # --seed 1234
107
+ config.order = :random
108
+
109
+ # Seed global randomization in this process using the `--seed` CLI option.
110
+ # Setting this allows you to use `--seed` to deterministically reproduce
111
+ # test failures related to randomization by passing the same `--seed` value
112
+ # as the one that triggered the failure.
113
+ Kernel.srand config.seed
114
+ =end
115
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chronos_authz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Marianne Angelie del Mundo
8
+ - Rodette Pedro
9
+ - JR Respino
10
+ - Jayson Uy
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2018-08-10 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: railties
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: '4.2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: request_store
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ - !ruby/object:Gem::Dependency
45
+ name: activesupport
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ - !ruby/object:Gem::Dependency
59
+ name: rake
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 11.3.0
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 11.3.0
72
+ - !ruby/object:Gem::Dependency
73
+ name: rspec-rails
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: simplecov
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: simplecov-console
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ description: A declarative authorization Rack middleware that supports custom authorization
115
+ logic on a per-resource basis
116
+ email:
117
+ - marianne@chronoscloud.com
118
+ - rodette@chronoscloud.com
119
+ - jr@chronoscloud.com
120
+ - jayson@chronoscloud.com
121
+ executables: []
122
+ extensions: []
123
+ extra_rdoc_files: []
124
+ files:
125
+ - ".gitignore"
126
+ - ".rspec"
127
+ - Gemfile
128
+ - Gemfile.lock
129
+ - README.md
130
+ - Rakefile
131
+ - chronos_authz.gemspec
132
+ - lib/chronos_authz.rb
133
+ - lib/chronos_authz/acl.rb
134
+ - lib/chronos_authz/authorizer.rb
135
+ - lib/chronos_authz/configuration.rb
136
+ - lib/chronos_authz/rule.rb
137
+ - lib/chronos_authz/user.rb
138
+ - lib/chronos_authz/validations/options_validator.rb
139
+ - lib/chronos_authz/validations/validation_error.rb
140
+ - lib/chronos_authz/version.rb
141
+ - spec/acl_spec.rb
142
+ - spec/authorizer_spec.rb
143
+ - spec/config/authorizer_acl_test.yml
144
+ - spec/config/error_page.html
145
+ - spec/helpers/custom_rule.rb
146
+ - spec/options_validator_spec.rb
147
+ - spec/spec_helper.rb
148
+ homepage: https://github.com/chronoscloud/chronoscloud-authz
149
+ licenses:
150
+ - N/A
151
+ metadata: {}
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '2.4'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubyforge_project:
168
+ rubygems_version: 2.6.8
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: A minimal and declarative authorization layer
172
+ test_files:
173
+ - spec/acl_spec.rb
174
+ - spec/authorizer_spec.rb
175
+ - spec/config/authorizer_acl_test.yml
176
+ - spec/config/error_page.html
177
+ - spec/helpers/custom_rule.rb
178
+ - spec/options_validator_spec.rb
179
+ - spec/spec_helper.rb