rails-auth 1.3.0 → 2.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.
@@ -3,5 +3,8 @@
3
3
  # Pull in core library components that work with any Rack application
4
4
  require "rails/auth/rack"
5
5
 
6
+ # Rails configuration builder
7
+ require "rails/auth/config_builder"
8
+
6
9
  # Rails controller method support
7
10
  require "rails/auth/controller_methods"
@@ -1,4 +1,4 @@
1
- # Pull in default predicate matchers
1
+ # Pull in default matchers
2
2
  require "rails/auth/acl/matchers/allow_all"
3
3
 
4
4
  module Rails
@@ -7,7 +7,7 @@ module Rails
7
7
  class ACL
8
8
  attr_reader :resources
9
9
 
10
- # Predicate matchers available by default in ACLs
10
+ # Matchers available by default in ACLs
11
11
  DEFAULT_MATCHERS = {
12
12
  allow_all: Matchers::AllowAll
13
13
  }.freeze
@@ -21,7 +21,7 @@ module Rails
21
21
  end
22
22
 
23
23
  # @param [Array<Hash>] :acl Access Control List configuration
24
- # @param [Hash] :matchers predicate matchers for use with this ACL
24
+ # @param [Hash] :matchers authorizers use with this ACL
25
25
  #
26
26
  def initialize(acl, matchers: {})
27
27
  raise TypeError, "expected Array for acl, got #{acl.class}" unless acl.is_a?(Array)
@@ -34,32 +34,37 @@ module Rails
34
34
  resources = entry["resources"]
35
35
  raise ParseError, "no 'resources' key present in entry: #{entry.inspect}" unless resources
36
36
 
37
- predicates = parse_predicates(entry, matchers.merge(DEFAULT_MATCHERS))
37
+ matcher_instances = parse_matchers(entry, matchers.merge(DEFAULT_MATCHERS))
38
38
 
39
39
  resources.each do |resource|
40
- @resources << Resource.new(resource, predicates).freeze
40
+ @resources << Resource.new(resource, matcher_instances).freeze
41
41
  end
42
42
  end
43
43
 
44
44
  @resources.freeze
45
45
  end
46
46
 
47
- # Match the Rack environment against the ACL, checking all predicates
47
+ # Match the Rack environment against the ACL, checking all matchers
48
48
  #
49
49
  # @param [Hash] :env Rack environment
50
50
  #
51
- # @return [Boolean] is the request authorized?
51
+ # @return [String, nil] name of the first matching matcher, or nil if unauthorized
52
52
  #
53
53
  def match(env)
54
- @resources.any? { |resource| resource.match(env) }
54
+ @resources.each do |resource|
55
+ matcher_name = resource.match(env)
56
+ return matcher_name if matcher_name
57
+ end
58
+
59
+ nil
55
60
  end
56
61
 
57
- # Find all resources that match the ACL. Predicates are *NOT* checked,
62
+ # Find all resources that match the ACL. Matchers are *NOT* checked,
58
63
  # instead only the initial checks for the "resources" section of the ACL
59
- # are performed. Use the `#match` method to validate predicates.
64
+ # are performed. Use the `#match` method to validate matchers.
60
65
  #
61
66
  # This method is intended for debugging AuthZ failures. It can find all
62
- # resources that match the given request so the corresponding predicates
67
+ # resources that match the given request so the corresponding matchers
63
68
  # can be introspected.
64
69
  #
65
70
  # @param [Hash] :env Rack environment
@@ -72,8 +77,8 @@ module Rails
72
77
 
73
78
  private
74
79
 
75
- def parse_predicates(entry, matchers)
76
- predicates = {}
80
+ def parse_matchers(entry, matchers)
81
+ matcher_instances = {}
77
82
 
78
83
  entry.each do |name, options|
79
84
  next if name == "resources"
@@ -82,10 +87,10 @@ module Rails
82
87
  raise ArgumentError, "no matcher for #{name}" unless matcher_class
83
88
  raise TypeError, "expected Class for #{name}" unless matcher_class.is_a?(Class)
84
89
 
85
- predicates[name.freeze] = matcher_class.new(options.freeze).freeze
90
+ matcher_instances[name.freeze] = matcher_class.new(options.freeze).freeze
86
91
  end
87
92
 
88
- predicates.freeze
93
+ matcher_instances.freeze
89
94
  end
90
95
  end
91
96
  end
@@ -1,7 +1,7 @@
1
1
  module Rails
2
2
  module Auth
3
3
  class ACL
4
- # Built-in predicate matchers
4
+ # Built-in matchers
5
5
  module Matchers
6
6
  # Allows unauthenticated clients to access to a given resource
7
7
  class AllowAll
@@ -22,7 +22,12 @@ module Rails
22
22
  end
23
23
 
24
24
  def call(env)
25
- raise NotAuthorizedError, "unauthorized request" unless Rails::Auth.authorized?(env) || @acl.match(env)
25
+ unless Rails::Auth.authorized?(env)
26
+ matcher_name = @acl.match(env)
27
+ raise NotAuthorizedError, "unauthorized request" unless matcher_name
28
+ Rails::Auth.set_allowed_by(env, "matcher:#{matcher_name}")
29
+ end
30
+
26
31
  @app.call(env)
27
32
  end
28
33
  end
@@ -5,7 +5,7 @@ module Rails
5
5
  class ACL
6
6
  # Rules for a particular route
7
7
  class Resource
8
- attr_reader :http_methods, :path, :host, :predicates
8
+ attr_reader :http_methods, :path, :host, :matchers
9
9
 
10
10
  # Valid HTTP methods
11
11
  HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK).freeze
@@ -15,11 +15,11 @@ module Rails
15
15
 
16
16
  # @option :options [String] :method HTTP method allowed ("ALL" for all methods)
17
17
  # @option :options [String] :path path to the resource (regex syntax allowed)
18
- # @param [Hash] :predicates matchers for this resource
18
+ # @param [Hash] :matchers which matchers are used for this resource
19
19
  #
20
- def initialize(options, predicates)
21
- raise TypeError, "expected Hash for options" unless options.is_a?(Hash)
22
- raise TypeError, "expected Hash for predicates" unless predicates.is_a?(Hash)
20
+ def initialize(options, matchers)
21
+ raise TypeError, "expected Hash for options" unless options.is_a?(Hash)
22
+ raise TypeError, "expected Hash for matchers" unless matchers.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}"
@@ -30,7 +30,7 @@ module Rails
30
30
 
31
31
  @http_methods = extract_methods(methods)
32
32
  @path = /\A#{path}\z/
33
- @predicates = predicates.freeze
33
+ @matchers = matchers.freeze
34
34
 
35
35
  # Unlike method and path, host is optional
36
36
  host = options["host"]
@@ -38,19 +38,20 @@ module Rails
38
38
  end
39
39
 
40
40
  # Match this resource against the given Rack environment, checking all
41
- # predicates to ensure at least one of them matches
41
+ # matchers to ensure at least one of them matches
42
42
  #
43
43
  # @param [Hash] :env Rack environment
44
44
  #
45
- # @return [Boolean] resource and predicates match the given request
45
+ # @return [String, nil] name of the matcher which matched, or nil if none matched
46
46
  #
47
47
  def match(env)
48
- return false unless match!(env)
49
- @predicates.any? { |_name, predicate| predicate.match(env) }
48
+ return nil unless match!(env)
49
+ name, = @matchers.find { |_name, matcher| matcher.match(env) }
50
+ name
50
51
  end
51
52
 
52
53
  # Match *only* the request method/path/host against the given Rack environment.
53
- # Predicates are NOT checked.
54
+ # matchers are NOT checked.
54
55
  #
55
56
  # @param [Hash] :env Rack environment
56
57
  #
@@ -0,0 +1,83 @@
1
+ module Rails
2
+ module Auth
3
+ # Configures Rails::Auth middleware for use in a Rails application
4
+ module ConfigBuilder
5
+ extend self
6
+
7
+ # Application-level configuration (i.e. config/application.rb)
8
+ def application(config, acl_file: Rails.root.join("config/acl.yml"), matchers: {})
9
+ config.x.rails_auth.acl = Rails::Auth::ACL.from_yaml(
10
+ File.read(acl_file.to_s),
11
+ matchers: matchers
12
+ )
13
+
14
+ config.middleware.use Rails::Auth::ACL::Middleware, acl: config.x.acl
15
+ end
16
+
17
+ # Development configuration (i.e. config/environments/development.rb)
18
+ def development(config, development_credentials: {}, error_page: :debug)
19
+ error_page_middleware(config, error_page)
20
+ credential_injector_middleware(config, development_credentials) unless development_credentials.empty?
21
+ end
22
+
23
+ # Test configuration (i.e. config/environments/test.rb)
24
+ def test(config)
25
+ # Simulated credentials to be injected with InjectorMiddleware
26
+ credential_injector_middleware(config, config.x.rails_auth.test_credentials ||= {})
27
+ end
28
+
29
+ def production(
30
+ config,
31
+ cert_filters: nil,
32
+ require_cert: false,
33
+ ca_file: nil,
34
+ error_page: Rails.root.join("public/403.html"),
35
+ monitor: nil
36
+ )
37
+ raise ArgumentError, "no cert_filters given but require_cert is true" if require_cert && !cert_filters
38
+ raise ArgumentError, "no ca_file given but cert_filters were set" if cert_filters && !ca_file
39
+
40
+ error_page_middleware(config, error_page)
41
+
42
+ if cert_filters
43
+ config.middleware.insert_before Rails::Auth::ACL::Middleware,
44
+ Rails::Auth::X509::Middleware,
45
+ require_cert: require_cert,
46
+ cert_filters: cert_filters,
47
+ ca_file: ca_file,
48
+ logger: Rails.logger
49
+ end
50
+
51
+ return unless monitor
52
+ config.middleware.insert_before Rails::Auth::ACL::Middleware,
53
+ Rails::Auth::Monitor::Middleware,
54
+ monitor
55
+ end
56
+
57
+ private
58
+
59
+ # Adds error page middleware to the chain
60
+ def error_page_middleware(config, error_page)
61
+ case error_page
62
+ when :debug
63
+ config.middleware.insert_before Rails::Auth::ACL::Middleware,
64
+ Rails::Auth::ErrorPage::DebugMiddleware,
65
+ acl: config.x.rails_auth.acl
66
+ when Pathname, String
67
+ config.middleware.insert_before Rails::Auth::ACL::Middleware,
68
+ Rails::Auth::ErrorPage::Middleware,
69
+ page_body: Pathname(error_page).read
70
+ when FalseClass, NilClass
71
+ else raise TypeError, "bad error page mode: #{mode.inspect}"
72
+ end
73
+ end
74
+
75
+ # Adds Rails::Auth::Credentials::InjectorMiddleware to the chain with the given credentials
76
+ def credential_injector_middleware(config, credentials)
77
+ config.middleware.insert_before Rails::Auth::ACL::Middleware,
78
+ Rails::Auth::Credentials::InjectorMiddleware,
79
+ credentials
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,45 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Rails
4
6
  # Modular resource-based authentication and authorization for Rails/Rack
5
7
  module Auth
6
- # Rack environment key for all rails-auth credentials
7
- CREDENTIALS_ENV_KEY = "rails-auth.credentials".freeze
8
-
9
- # Functionality for storing credentials in the Rack environment
10
- module Credentials
11
- # Obtain credentials from a Rack environment
12
- #
13
- # @param [Hash] :env Rack environment
14
- #
15
- def credentials(env)
16
- env.fetch(CREDENTIALS_ENV_KEY, {})
17
- end
18
-
19
- # Add a credential to the Rack environment
20
- #
21
- # @param [Hash] :env Rack environment
22
- # @param [String] :type credential type to add to the environment
23
- # @param [Object] :credential object to add to the environment
24
- #
25
- def add_credential(env, type, credential)
26
- credentials = env[CREDENTIALS_ENV_KEY] ||= {}
8
+ # Stores a set of credentials
9
+ class Credentials
10
+ extend Forwardable
27
11
 
28
- # Adding a credential is idempotent, so attempting to reregister
29
- # the same credential should be harmless
30
- return env if credentials.key?(type) && credentials[type] == credential
12
+ def_delegators :@credentials, :[], :fetch, :empty?, :key?, :to_hash
31
13
 
32
- # raise if we already have a cred, but it didn't short-circuit as
33
- # being == to the one supplied
34
- raise ArgumentError, "credential #{type} already added to request" if credentials.key?(type)
14
+ def self.from_rack_env(env)
15
+ new(env.fetch(Rails::Auth::Env::CREDENTIALS_ENV_KEY, {}))
16
+ end
35
17
 
36
- credentials[type] = credential
18
+ def initialize(credentials = {})
19
+ raise TypeError, "expected Hash, got #{credentials.class}" unless credentials.is_a?(Hash)
20
+ @credentials = credentials
21
+ end
37
22
 
38
- env
23
+ def []=(type, value)
24
+ raise TypeError, "expected String for type, got #{type.class}" unless type.is_a?(String)
25
+ raise AlreadyAuthorizedError, "credential '#{type}' has already been set" if @credentials.key?(type)
26
+ @credentials[type] = value
39
27
  end
40
28
  end
41
-
42
- # Include these functions in Rails::Auth for convenience
43
- extend Credentials
44
29
  end
45
30
  end
@@ -1,6 +1,6 @@
1
1
  module Rails
2
2
  module Auth
3
- module Credentials
3
+ class Credentials
4
4
  # A middleware for injecting an arbitrary credentials hash into the Rack environment
5
5
  # This is intended for development and testing purposes where you would like to
6
6
  # simulate a given X.509 certificate being used in a request or user logged in
@@ -11,7 +11,7 @@ module Rails
11
11
  end
12
12
 
13
13
  def call(env)
14
- env[Rails::Auth::CREDENTIALS_ENV_KEY] = @credentials
14
+ env[Rails::Auth::Env::CREDENTIALS_ENV_KEY] = @credentials
15
15
  @app.call(env)
16
16
  end
17
17
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module Auth
5
+ # Wrapper for Rack environments with Rails::Auth helpers
6
+ class Env
7
+ # Rack environment key for marking external authorization
8
+ AUTHORIZED_ENV_KEY = "rails-auth.authorized".freeze
9
+
10
+ # Rack environment key for storing what allowed the request
11
+ ALLOWED_BY_ENV_KEY = "rails-auth.allowed-by".freeze
12
+
13
+ # Rack environment key for all rails-auth credentials
14
+ CREDENTIALS_ENV_KEY = "rails-auth.credentials".freeze
15
+
16
+ attr_reader :allowed_by, :credentials
17
+
18
+ # @param [Hash] :env Rack environment
19
+ def initialize(env, credentials: {}, authorized: false, allowed_by: nil)
20
+ raise TypeError, "expected Hash for credentials, got #{credentials.class}" unless credentials.is_a?(Hash)
21
+
22
+ @env = env
23
+ @credentials = Credentials.new(credentials.merge(@env.fetch(CREDENTIALS_ENV_KEY, {})))
24
+ @authorized = env.fetch(AUTHORIZED_ENV_KEY, authorized)
25
+ @allowed_by = env.fetch(ALLOWED_BY_ENV_KEY, allowed_by)
26
+ end
27
+
28
+ # Check whether a request has been authorized
29
+ def authorized?
30
+ @authorized
31
+ end
32
+
33
+ # Mark the environment as authorized to access the requested resource
34
+ #
35
+ # @param [String] :allowed_by label of what allowed the request
36
+ def authorize(allowed_by)
37
+ self.allowed_by = allowed_by
38
+ @authorized = true
39
+ end
40
+
41
+ # Set the name of the authority which authorized the request
42
+ #
43
+ # @param [String] :allowed_by label of what allowed the request
44
+ def allowed_by=(allowed_by)
45
+ raise AlreadyAuthorizedError, "already allowed by #{@allowed_by.ispect}" if @allowed_by
46
+ raise TypeError, "expected String for allowed_by, got #{allowed_by.class}" unless allowed_by.is_a?(String)
47
+ @allowed_by = allowed_by
48
+ end
49
+
50
+ # Return a Rack environment
51
+ #
52
+ # @return [Hash] Rack environment
53
+ def to_rack
54
+ credentials = @env[CREDENTIALS_ENV_KEY] ||= {}
55
+ credentials.merge!(@credentials.to_hash)
56
+
57
+ @env[AUTHORIZED_ENV_KEY] = @authorized if @authorized
58
+ @env[ALLOWED_BY_ENV_KEY] = @allowed_by if @allowed_by
59
+
60
+ @env
61
+ end
62
+ end
63
+ end
64
+ end
@@ -113,8 +113,8 @@
113
113
  <td class="label"><%= h((resource.http_methods || "ALL").join(" ")) %> <%= h(format_path(resource.path)) %></td>
114
114
  <td>
115
115
  <ul>
116
- <% resource.predicates.each do |name, predicate| %>
117
- <li><%= h(name) %>: <%= h(format_attributes(predicate)) %></li>
116
+ <% resource.matchers.each do |name, matcher| %>
117
+ <li><%= h(name) %>: <%= h(format_attributes(matcher)) %></li>
118
118
  <% end %>
119
119
  </ul>
120
120
  </td>
@@ -1,9 +1,15 @@
1
1
  module Rails
2
2
  module Auth
3
+ # Base class of all Rails::Auth errors
4
+ Error = Class.new(StandardError)
5
+
3
6
  # Unauthorized!
4
- NotAuthorizedError = Class.new(StandardError)
7
+ NotAuthorizedError = Class.new(Error)
5
8
 
6
9
  # Error parsing e.g. an ACL
7
- ParseError = Class.new(StandardError)
10
+ ParseError = Class.new(Error)
11
+
12
+ # Internal errors involving authorizing things that are already authorized
13
+ AlreadyAuthorizedError = Class.new(Error)
8
14
  end
9
15
  end
@@ -0,0 +1,64 @@
1
+ module Rails
2
+ # Modular resource-based authentication and authorization for Rails/Rack
3
+ module Auth
4
+ module_function
5
+
6
+ # Mark a request as externally authorized. Causes ACL checks to be skipped.
7
+ #
8
+ # @param [Hash] :rack_env Rack environment
9
+ # @param [String] :allowed_by what allowed the request
10
+ #
11
+ def authorized!(rack_env, allowed_by)
12
+ Env.new(rack_env).tap do |env|
13
+ env.authorize(allowed_by)
14
+ end.to_rack
15
+ end
16
+
17
+ # Check whether a request has been authorized
18
+ #
19
+ # @param [Hash] :rack_env Rack environment
20
+ #
21
+ def authorized?(rack_env)
22
+ Env.new(rack_env).authorized?
23
+ end
24
+
25
+ # Mark what authorized the request in the Rack environment
26
+ #
27
+ # @param [Hash] :env Rack environment
28
+ # @param [String] :allowed_by what allowed this request
29
+ def set_allowed_by(rack_env, allowed_by)
30
+ Env.new(rack_env).tap do |env|
31
+ env.allowed_by = allowed_by
32
+ end.to_rack
33
+ end
34
+
35
+ # Read what authorized the request
36
+ #
37
+ # @param [Hash] :rack_env Rack environment
38
+ #
39
+ # @return [String, nil] what authorized the request
40
+ def allowed_by(rack_env)
41
+ Env.new(rack_env).allowed_by
42
+ end
43
+
44
+ # Obtain credentials from a Rack environment
45
+ #
46
+ # @param [Hash] :rack_env Rack environment
47
+ #
48
+ def credentials(rack_env)
49
+ Credentials.from_rack_env(rack_env)
50
+ end
51
+
52
+ # Add a credential to the Rack environment
53
+ #
54
+ # @param [Hash] :rack_env Rack environment
55
+ # @param [String] :type credential type to add to the environment
56
+ # @param [Object] :credential object to add to the environment
57
+ #
58
+ def add_credential(rack_env, type, credential)
59
+ Env.new(rack_env).tap do |env|
60
+ env.credentials[type] = credential
61
+ end.to_rack
62
+ end
63
+ end
64
+ end