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 +4 -4
- data/CHANGES.md +6 -0
- data/Gemfile +1 -1
- data/README.md +39 -3
- data/images/debug_error_page.png +0 -0
- data/lib/rails/auth/acl.rb +3 -3
- data/lib/rails/auth/acl/matchers/allow_all.rb +1 -1
- data/lib/rails/auth/acl/middleware.rb +2 -2
- data/lib/rails/auth/acl/resource.rb +4 -4
- data/lib/rails/auth/credentials.rb +1 -1
- data/lib/rails/auth/error_page/debug_middleware.rb +58 -0
- data/lib/rails/auth/error_page/debug_page.html.erb +128 -0
- data/lib/rails/auth/error_page/middleware.rb +1 -1
- data/lib/rails/auth/rack.rb +1 -0
- data/lib/rails/auth/rspec/helper_methods.rb +1 -1
- data/lib/rails/auth/version.rb +1 -1
- data/lib/rails/auth/x509/certificate.rb +1 -1
- data/lib/rails/auth/x509/middleware.rb +3 -3
- data/spec/rails/auth/error_page/debug_middleware_spec.rb +37 -0
- data/spec/rails/auth/error_page/middleware_spec.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 80fe77f1e603f206962d83d87f214d2f57e1dee9
|
4
|
+
data.tar.gz: 1c3eb52e07760479ad2254428bce28527b4deab4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 18bdd7c51f0aa9844b0f7305f3a4455e93a6e3830722633924d433dd95ec608cd45354195007ca26a7020e9eb433f82ff8630a9bfe0b8b3a7a000f4024cb8bb9
|
7
|
+
data.tar.gz: 4d3b4b5146d98fbd5335944bbb5bba3a233f886b325430058415e4e5ca6961f273dfb9cd80744e8f0ccd839d4a12c1779b208cb6864f2df819be9fa1a2d511cc
|
data/CHANGES.md
CHANGED
data/Gemfile
CHANGED
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,
|
410
|
-
|
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
|
-
|
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
|
data/lib/rails/auth/acl.rb
CHANGED
@@ -26,7 +26,7 @@ module Rails
|
|
26
26
|
|
27
27
|
acl.each_with_index do |entry|
|
28
28
|
resources = entry["resources"]
|
29
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/rails/auth/rack.rb
CHANGED
data/lib/rails/auth/version.rb
CHANGED
@@ -9,7 +9,7 @@ module Rails
|
|
9
9
|
|
10
10
|
def initialize(certificate)
|
11
11
|
unless certificate.is_a?(OpenSSL::X509::Certificate)
|
12
|
-
|
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
|
-
|
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
|
-
|
60
|
+
raise CertificateVerifyFailed, "verify failed: #{subject(cert)}" if @require_cert
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
|
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) {
|
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.
|
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-
|
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:
|