rails-auth 0.2.0 → 0.3.0
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 +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
|
+

|
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:
|