rails-auth 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 98e7a606fb32ef7873566d049507dae530d17dc5
4
- data.tar.gz: 3944a0e2f6d940b3e2dd64404ab96dab88a6be29
3
+ metadata.gz: 80fe77f1e603f206962d83d87f214d2f57e1dee9
4
+ data.tar.gz: 1c3eb52e07760479ad2254428bce28527b4deab4
5
5
  SHA512:
6
- metadata.gz: 396fa09d53a3fc2d646e0b3822ecb40867de7010cd54bac5ab052a98fa0a05593b82fd7bcc870ebebd69420f866d3ccb1218990317b148223f2caebe9d48a047
7
- data.tar.gz: 3e5ab68b0a75a8b1ef877c31a26f692bea39dcc4cfcd751fbb3fe9b5ea8ac9ee9d0d2056b7c09bf64b3ab64d1cead82507adb890a72186a845fc6078357dd7b6
6
+ metadata.gz: 18bdd7c51f0aa9844b0f7305f3a4455e93a6e3830722633924d433dd95ec608cd45354195007ca26a7020e9eb433f82ff8630a9bfe0b8b3a7a000f4024cb8bb9
7
+ data.tar.gz: 4d3b4b5146d98fbd5335944bbb5bba3a233f886b325430058415e4e5ca6961f273dfb9cd80744e8f0ccd839d4a12c1779b208cb6864f2df819be9fa1a2d511cc
data/CHANGES.md CHANGED
@@ -1,3 +1,9 @@
1
+ ### 0.3.0 (2016-03-12)
2
+
3
+ * [#12](https://github.com/square/rails-auth/pull/12)
4
+ Add Rails::Auth::ErrorPage::DebugMiddleware.
5
+ ([@tarcieri])
6
+
1
7
  ### 0.2.0 (2016-03-11)
2
8
 
3
9
  * [#10](https://github.com/square/rails-auth/pull/10)
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ end
6
6
 
7
7
  group :test do
8
8
  gem "rspec"
9
- gem "rubocop", "0.36.0"
9
+ gem "rubocop", "0.38.0"
10
10
  gem "coveralls", require: false
11
11
  gem "certificate_authority", require: false
12
12
  end
data/README.md CHANGED
@@ -406,10 +406,46 @@ exception is raised up the middleware chain. However, it's likely you would
406
406
  prefer to show an error page than have an unhandled exception.
407
407
 
408
408
  You can write your own middleware that catches `Rails::Auth::NotAuthorizedError`
409
- if you'd like. However, a default one is provided which renders a 403 response
410
- with a static page body if you find that helpful.
409
+ if you'd like. However, this library includes two middleware for rescuing this
410
+ exception for you and displaying an error page.
411
411
 
412
- To use it, add `Rails::Auth::ErrorPage::Middleware` to your app:
412
+ #### Rails::Auth::ErrorPage::DebugMiddleware
413
+
414
+ This middleware displays a detailed error page intended to help debug authorization errors:
415
+
416
+ ![Debug Error Page](https://raw.github.com/square/rails-auth/master/images/debug_error_page.png)
417
+
418
+ Please be aware this middleware leaks information about your ACL to a potential attacker.
419
+ Make sure you're ok with that information being public before using it. If you would like
420
+ to avoid leaking that information, see `Rails::Auth::ErrorPage::Middleware` below.
421
+
422
+ ```ruby
423
+ app = MyRackApp.new
424
+
425
+ acl = Rails::Auth::ACL.from_yaml(
426
+ File.read("/path/to/my/acl.yaml")
427
+ matchers: { allow_x509_subject: Rails::Auth::X509::Matcher }
428
+ )
429
+
430
+ acl_auth = Rails::Auth::ACL::Middleware.new(app, acl: acl)
431
+
432
+ x509_auth = Rails::Auth::X509::Middleware.new(
433
+ acl_auth,
434
+ ca_file: "/path/to/my/cabundle.pem"
435
+ cert_filters: { 'X-SSL-Client-Cert' => :pem },
436
+ require_cert: true
437
+ )
438
+
439
+ error_page = Rails::Auth::ErrorPage::Middleware.new(x509_auth, acl: acl)
440
+
441
+ run error_page
442
+ ```
443
+
444
+ #### Rails::Auth::ErrorPage::Middleware
445
+
446
+ This middleware catches `Rails::Auth::NotAuthorizedError` and renders a given static HTML file,
447
+ e.g. the 403.html file which ships with Rails. It will not give detailed errors to your users,
448
+ but it also won't leak information to an attacker.
413
449
 
414
450
  ```ruby
415
451
  app = MyRackApp.new
Binary file
@@ -26,7 +26,7 @@ module Rails
26
26
 
27
27
  acl.each_with_index do |entry|
28
28
  resources = entry["resources"]
29
- fail ParseError, "no 'resources' key present in entry: #{entry.inspect}" unless resources
29
+ raise ParseError, "no 'resources' key present in entry: #{entry.inspect}" unless resources
30
30
 
31
31
  predicates = parse_predicates(entry, matchers.merge(DEFAULT_MATCHERS))
32
32
 
@@ -73,8 +73,8 @@ module Rails
73
73
  next if name == "resources"
74
74
 
75
75
  matcher_class = matchers[name.to_sym]
76
- fail ArgumentError, "no matcher for #{name}" unless matcher_class
77
- fail TypeError, "expected Class for #{name}" unless matcher_class.is_a?(Class)
76
+ raise ArgumentError, "no matcher for #{name}" unless matcher_class
77
+ raise TypeError, "expected Class for #{name}" unless matcher_class.is_a?(Class)
78
78
 
79
79
  predicates[name.freeze] = matcher_class.new(options.freeze).freeze
80
80
  end
@@ -6,7 +6,7 @@ module Rails
6
6
  # Allows unauthenticated clients to access to a given resource
7
7
  class AllowAll
8
8
  def initialize(enabled)
9
- fail ArgumentError, "enabled must be true/false" unless [true, false].include?(enabled)
9
+ raise ArgumentError, "enabled must be true/false" unless [true, false].include?(enabled)
10
10
  @enabled = enabled
11
11
  end
12
12
 
@@ -15,14 +15,14 @@ module Rails
15
15
  #
16
16
  # @return [Rails::Auth::ACL::Middleware] new ACL middleware instance
17
17
  def initialize(app, acl: nil)
18
- fail ArgumentError, "no acl given" unless acl
18
+ raise ArgumentError, "no acl given" unless acl
19
19
 
20
20
  @app = app
21
21
  @acl = acl
22
22
  end
23
23
 
24
24
  def call(env)
25
- fail NotAuthorizedError, "unauthorized request" unless @acl.match(env)
25
+ raise NotAuthorizedError, "unauthorized request" unless @acl.match(env)
26
26
  @app.call(env)
27
27
  end
28
28
  end
@@ -18,11 +18,11 @@ module Rails
18
18
  # @param [Hash] :predicates matchers for this resource
19
19
  #
20
20
  def initialize(options, predicates)
21
- fail TypeError, "expected Hash for options" unless options.is_a?(Hash)
22
- fail TypeError, "expected Hash for predicates" unless predicates.is_a?(Hash)
21
+ raise TypeError, "expected Hash for options" unless options.is_a?(Hash)
22
+ raise TypeError, "expected Hash for predicates" unless predicates.is_a?(Hash)
23
23
 
24
24
  unless (extra_keys = options.keys - VALID_OPTIONS).empty?
25
- fail ParseError, "unrecognized key in ACL resource: #{extra_keys.first}"
25
+ raise ParseError, "unrecognized key in ACL resource: #{extra_keys.first}"
26
26
  end
27
27
 
28
28
  @http_methods = extract_methods(options["method"])
@@ -63,7 +63,7 @@ module Rails
63
63
  return nil if methods.include?("ALL")
64
64
 
65
65
  methods.each do |method|
66
- fail ParseError, "invalid HTTP method: #{method}" unless HTTP_METHODS.include?(method)
66
+ raise ParseError, "invalid HTTP method: #{method}" unless HTTP_METHODS.include?(method)
67
67
  end
68
68
 
69
69
  methods.freeze
@@ -25,7 +25,7 @@ module Rails
25
25
  def add_credential(env, type, credential)
26
26
  credentials = env[CREDENTIALS_ENV_KEY] ||= {}
27
27
 
28
- fail ArgumentError, "credential #{type} already added to request" if credentials.key?(type)
28
+ raise ArgumentError, "credential #{type} already added to request" if credentials.key?(type)
29
29
  credentials[type] = credential
30
30
 
31
31
  env
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "cgi"
5
+
6
+ module Rails
7
+ module Auth
8
+ module ErrorPage
9
+ # Render a descriptive access denied page with debugging information about why the given
10
+ # request was not authorized. Useful for debugging, but leaks information about your ACL
11
+ # to a potential attacker. Make sure you're ok with that information being public.
12
+ class DebugMiddleware
13
+ # Configure CSP to disable JavaScript, but allow inline CSS
14
+ # This is just in case someone pulls off reflective XSS, but hopefully all values are
15
+ # properly escaped on the page so that won't happen.
16
+ RESPONSE_HEADERS = {
17
+ "Content-Security-Policy" =>
18
+ "default-src 'self'; " \
19
+ "script-src 'none'; " \
20
+ "style-src 'unsafe-inline'"
21
+ }.freeze
22
+
23
+ def initialize(app, acl: nil)
24
+ raise ArgumentError, "ACL must be a Rails::Auth::ACL" unless acl.is_a?(Rails::Auth::ACL)
25
+
26
+ @app = app
27
+ @acl = acl
28
+ @erb = ERB.new(File.read(File.expand_path("../debug_page.html.erb", __FILE__))).freeze
29
+ end
30
+
31
+ def call(env)
32
+ @app.call(env)
33
+ rescue Rails::Auth::NotAuthorizedError
34
+ [403, RESPONSE_HEADERS.dup, [error_page(env)]]
35
+ end
36
+
37
+ def error_page(env)
38
+ credentials = Rails::Auth.credentials(env)
39
+ resources = @acl.matching_resources(env)
40
+
41
+ @erb.result(binding)
42
+ end
43
+
44
+ def h(text)
45
+ CGI.escapeHTML(text || "")
46
+ end
47
+
48
+ def format_attributes(value)
49
+ value.respond_to?(:attributes) ? value.attributes.inspect : value.inspect
50
+ end
51
+
52
+ def format_path(path)
53
+ path.source.sub(/\A\\A/, "").sub(/\\z\z/, "")
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,128 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Rails::Auth: Access Denied</title>
6
+ <style>
7
+ body {
8
+ font-family: Helvetica, Arial, sans-serif;
9
+ margin: 0;
10
+ }
11
+
12
+ #header {
13
+ background-color: #ddd;
14
+ padding: 8px 32px;
15
+ border-bottom: 2px solid #888;
16
+ }
17
+
18
+ #header h1, #header h2 {
19
+ margin: 0;
20
+ font-size: 2em;
21
+ display: inline-block;
22
+ }
23
+
24
+ #header h2 {
25
+ color: #880000;
26
+ }
27
+
28
+ #content {
29
+ padding: 8px 32px;
30
+ }
31
+
32
+ table {
33
+ border: 1px solid #ddd;
34
+ border-collapse: collapse;
35
+ }
36
+
37
+ tr:nth-child(odd) {
38
+ background-color: #eee;
39
+ }
40
+
41
+ td {
42
+ padding: 6px;
43
+ border-bottom: 1px solid #ddd;
44
+ }
45
+
46
+ td.label {
47
+ font-weight: bold;
48
+ text-align: right;
49
+ }
50
+ </style>
51
+ </head>
52
+
53
+ <body>
54
+ <div id="header">
55
+ <h1>Rails::Auth:</h1>
56
+ <h2>Access Denied</h2>
57
+ </div>
58
+
59
+ <div id="content">
60
+ <div id="row">
61
+ <p><b>Error:</b> Access to the requested resource is not allowed with your current credentials.</p>
62
+ <p>Below is information about the request you made and the credentials you sent:</p>
63
+ </div>
64
+
65
+ <div id="row">
66
+ <h3>Request:</h3>
67
+
68
+ <table>
69
+ <tr>
70
+ <td class="label">Method</td>
71
+ <td><%= h(env["REQUEST_METHOD"]) %></td>
72
+ </tr>
73
+ <tr>
74
+ <td class="label">Path</td>
75
+ <td><%= h(env["REQUEST_PATH"]) %></td>
76
+ </tr>
77
+ <tr>
78
+ <td class="label">Host</td>
79
+ <td><%= h(env["HTTP_HOST"]) %></td>
80
+ </tr>
81
+ </table>
82
+ </div>
83
+
84
+ <div id="row">
85
+ <h3>Credentials:</h3>
86
+
87
+ <% if credentials.empty? %>
88
+ <p><b>No credentials provided!</b> This is a likely cause of this error.</p>
89
+ <p>Please retry the request with proper credentials.</p>
90
+ <% else %>
91
+ <table>
92
+ <% credentials.each do |name, credential| %>
93
+ <tr>
94
+ <td class="label"><%= h(name) %></td>
95
+ <td><%= h(format_attributes(credential)) %></td>
96
+ </tr>
97
+ <% end %>
98
+ </table>
99
+ <% end %>
100
+ </div>
101
+
102
+ <div id="row">
103
+ <h3>Authorized ACL Entries:</h2>
104
+ <% if resources.empty? %>
105
+ <p><b>Error: No matching resources!</b> This is a likely cause of this error.</p>
106
+ <p>Please check your ACL and make sure there's an entry for this route.</p>
107
+ <% else %>
108
+ <p>The following entries in your ACL are authorized to view this paritcular route:</p>
109
+
110
+ <table>
111
+ <% resources.each do |resource| %>
112
+ <tr>
113
+ <td class="label"><%= h((resource.http_methods || "ALL").join(" ")) %> <%= h(format_path(resource.path)) %></td>
114
+ <td>
115
+ <ul>
116
+ <% resource.predicates.each do |name, predicate| %>
117
+ <li><%= h(name) %>: <%= h(format_attributes(predicate)) %></li>
118
+ <% end %>
119
+ </ul>
120
+ </td>
121
+ </tr>
122
+ <% end %>
123
+ </table>
124
+ <% end %>
125
+ </div>
126
+ </div>
127
+ </body>
128
+ </html>
@@ -4,7 +4,7 @@ module Rails
4
4
  # Render an error page in the event Rails::Auth::NotAuthorizedError is raised
5
5
  class Middleware
6
6
  def initialize(app, page_body: nil)
7
- fail TypeError, "page_body must be a String" unless page_body.is_a?(String)
7
+ raise TypeError, "page_body must be a String" unless page_body.is_a?(String)
8
8
 
9
9
  @app = app
10
10
  @page_body = page_body.freeze
@@ -14,6 +14,7 @@ require "rails/auth/acl/middleware"
14
14
  require "rails/auth/acl/resource"
15
15
 
16
16
  require "rails/auth/error_page/middleware"
17
+ require "rails/auth/error_page/debug_middleware"
17
18
 
18
19
  require "rails/auth/x509/certificate"
19
20
  require "rails/auth/x509/filter/pem"
@@ -30,7 +30,7 @@ module Rails
30
30
 
31
31
  # Warn if methods are improperly used
32
32
  unless path.chars[0] == "/"
33
- fail ArgumentError, "expected #{path} to start with '/'"
33
+ raise ArgumentError, "expected #{path} to start with '/'"
34
34
  end
35
35
 
36
36
  env = {
@@ -3,6 +3,6 @@
3
3
  module Rails
4
4
  # Pluggable authentication and authorization for Rack/Rails
5
5
  module Auth
6
- VERSION = "0.2.0".freeze
6
+ VERSION = "0.3.0".freeze
7
7
  end
8
8
  end
@@ -9,7 +9,7 @@ module Rails
9
9
 
10
10
  def initialize(certificate)
11
11
  unless certificate.is_a?(OpenSSL::X509::Certificate)
12
- fail TypeError, "expecting OpenSSL::X509::Certificate, got #{certificate.class}"
12
+ raise TypeError, "expecting OpenSSL::X509::Certificate, got #{certificate.class}"
13
13
  end
14
14
 
15
15
  @certificate = certificate.freeze
@@ -19,7 +19,7 @@ module Rails
19
19
  #
20
20
  # @return [Rails::Auth::X509::Middleware] new X509 middleware instance
21
21
  def initialize(app, cert_filters: {}, ca_file: nil, truststore: nil, require_cert: false, logger: nil)
22
- fail ArgumentError, "no ca_file given" unless ca_file
22
+ raise ArgumentError, "no ca_file given" unless ca_file
23
23
 
24
24
  @app = app
25
25
  @logger = logger
@@ -57,11 +57,11 @@ module Rails
57
57
  return Rails::Auth::X509::Certificate.new(cert)
58
58
  else
59
59
  log("Verify FAILED", cert)
60
- fail CertificateVerifyFailed, "verify failed: #{subject(cert)}" if @require_cert
60
+ raise CertificateVerifyFailed, "verify failed: #{subject(cert)}" if @require_cert
61
61
  end
62
62
  end
63
63
 
64
- fail CertificateVerifyFailed, "no client certificate in request" if @require_cert
64
+ raise CertificateVerifyFailed, "no client certificate in request" if @require_cert
65
65
  nil
66
66
  end
67
67
 
@@ -0,0 +1,37 @@
1
+ RSpec.describe Rails::Auth::ErrorPage::DebugMiddleware do
2
+ let(:request) { Rack::MockRequest.env_for("https://www.example.com") }
3
+
4
+ let(:example_config) { fixture_path("example_acl.yml").read }
5
+
6
+ let(:example_acl) do
7
+ Rails::Auth::ACL.from_yaml(
8
+ example_config,
9
+ matchers: {
10
+ allow_x509_subject: Rails::Auth::X509::Matcher,
11
+ allow_claims: ClaimsMatcher
12
+ }
13
+ )
14
+ end
15
+
16
+ subject(:middleware) { described_class.new(app, acl: example_acl) }
17
+
18
+ context "access granted" do
19
+ let(:code) { 200 }
20
+ let(:app) { ->(env) { [code, env, "Hello, world!"] } }
21
+
22
+ it "renders the expected response" do
23
+ response = middleware.call(request)
24
+ expect(response.first).to eq code
25
+ end
26
+ end
27
+
28
+ context "access denied" do
29
+ let(:app) { ->(_env) { raise(Rails::Auth::NotAuthorizedError, "not authorized!") } }
30
+
31
+ it "renders the error page" do
32
+ code, _env, body = middleware.call(request)
33
+ expect(code).to eq 403
34
+ expect(body.join).to include("Access Denied")
35
+ end
36
+ end
37
+ end
@@ -15,7 +15,7 @@ RSpec.describe Rails::Auth::ErrorPage::Middleware do
15
15
  end
16
16
 
17
17
  context "access denied" do
18
- let(:app) { ->(_env) { fail(Rails::Auth::NotAuthorizedError, "not authorized!") } }
18
+ let(:app) { ->(_env) { raise(Rails::Auth::NotAuthorizedError, "not authorized!") } }
19
19
 
20
20
  it "renders the error page" do
21
21
  code, _env, body = middleware.call(request)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-03-12 00:00:00.000000000 Z
11
+ date: 2016-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -74,6 +74,7 @@ files:
74
74
  - LICENSE
75
75
  - README.md
76
76
  - Rakefile
77
+ - images/debug_error_page.png
77
78
  - lib/rails/auth.rb
78
79
  - lib/rails/auth/acl.rb
79
80
  - lib/rails/auth/acl/matchers/allow_all.rb
@@ -81,6 +82,8 @@ files:
81
82
  - lib/rails/auth/acl/resource.rb
82
83
  - lib/rails/auth/controller_methods.rb
83
84
  - lib/rails/auth/credentials.rb
85
+ - lib/rails/auth/error_page/debug_middleware.rb
86
+ - lib/rails/auth/error_page/debug_page.html.erb
84
87
  - lib/rails/auth/error_page/middleware.rb
85
88
  - lib/rails/auth/exceptions.rb
86
89
  - lib/rails/auth/rack.rb
@@ -101,6 +104,7 @@ files:
101
104
  - spec/rails/auth/acl_spec.rb
102
105
  - spec/rails/auth/controller_methods_spec.rb
103
106
  - spec/rails/auth/credentials_spec.rb
107
+ - spec/rails/auth/error_page/debug_middleware_spec.rb
104
108
  - spec/rails/auth/error_page/middleware_spec.rb
105
109
  - spec/rails/auth/rspec/helper_methods_spec.rb
106
110
  - spec/rails/auth/rspec/matchers/acl_matchers_spec.rb
@@ -137,3 +141,4 @@ signing_key:
137
141
  specification_version: 4
138
142
  summary: Modular resource-oriented authentication and authorization for Rails/Rack
139
143
  test_files: []
144
+ has_rdoc: