Skip to content
This repository has been archived by the owner on Mar 28, 2023. It is now read-only.

Commit

Permalink
f
Browse files Browse the repository at this point in the history
  • Loading branch information
bartes committed Nov 7, 2018
1 parent e840c1f commit fd76c9d
Show file tree
Hide file tree
Showing 18 changed files with 264 additions and 210 deletions.
83 changes: 47 additions & 36 deletions lib/castle/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,68 @@

require 'castle/middleware/configuration'
require 'castle/middleware/configuration_options'
require 'castle/middleware/configuration_services'
require 'castle/middleware/event_mapper'
require 'castle/middleware/params_flattener'
require 'castle/middleware/sensor'
require 'castle/middleware/tracking'
require 'castle/middleware/authenticating'
require 'castle/middleware/version'
require 'singleton'

module Castle
# Main middleware definition
module Middleware
class << self
attr_writer :configuration
class Middleware
include ::Singleton

def call_error_handler(exception)
return unless configuration.error_handler
configuration.error_handler.call(exception)
end
def call_error_handler(exception)
return unless configuration.services.error_handler
configuration.services.error_handler.call(exception)
end

def configure
raise ArgumentError unless block_given?
@configuration_options = ConfigurationOptions.new
yield(@configuration_options)
@event_mapping = nil
end
def configure
raise ArgumentError unless block_given?
@configuration_options = ConfigurationOptions.new
yield(@configuration_options)
@event_mapping = nil
end

def configuration
@configuration ||= Configuration.new(@configuration_options)
end
def configuration=(value)
@configuration = value
end

def event_mapping
@event_mapping ||= EventMapper.build(configuration.events)
end
def configuration
@configuration ||= Configuration.new(@configuration_options)
end

def log(level, message)
return unless Middleware.configuration.logger
Middleware.configuration.logger.public_send(level.to_s, message)
end
def event_mapping
@event_mapping ||= EventMapper.build(configuration.events)
end

def track(context, options)
log(:debug, "[Castle] Tracking #{options[:event]}")
::Castle::Client.new(context).track(options)
rescue Castle::Error => e
log(:warn, "[Castle] Can't send tracking request because #{e} exception")
call_error_handler(e)
end
def log(level, message)
return unless configuration.logger
configuration.logger.public_send(level.to_s, message)
end

def authenticate(context, options)
log(:debug, "[Castle] Authenticating #{options[:event]}")
::Castle::Client.new(context).authenticate(options)
rescue Castle::Error => e
log(:warn, "[Castle] Can't send authenticating request because #{e} exception")
call_error_handler(e)
def track(context, options)
log(:debug, "[Castle] Tracking #{options[:event]}")
::Castle::Client.new(context).track(options)
rescue Castle::Error => e
log(:warn, "[Castle] Can't send tracking request because #{e} exception")
call_error_handler(e)
end

def authenticate(context, options)
log(:debug, "[Castle] Authenticating #{options[:event]}")
::Castle::Client.new(context).authenticate(options)
rescue Castle::Error => e
log(:warn, "[Castle] Can't send authenticating request because #{e} exception")
call_error_handler(e)
end

class << self
def configure(&block)
instance.configure(&block)
end
end
end
Expand Down
79 changes: 79 additions & 0 deletions lib/castle/middleware/authenticating.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require 'castle/middleware/request_config'

module Castle
class Middleware
class Authenticating
extend Forwardable
def_delegators :@middleware, :log, :configuration, :event_mapping, :authenticate

attr_reader :app

def initialize(app)
@app = app
@middleware = Middleware.instance
end

def call(env)
env['castle'] = RequestConfig.new
req = Rack::Request.new(env)

if login?(req)
byebug
redirect_result = authentication_verdict(req, env)
return [301, {'Location' => redirect_result}, []] if redirect_result
end

env['castle'].identify(req.session['castle_user_id'], {}) if req.session['castle_user_id']

# [status, headers, body]
app.call(env)
end

private

def authentication_verdict(req, env)
key = req.params.dig(*configuration.login.dig('authentication', 'key').split('.'))
pass = req.params.dig(*configuration.login.dig('authentication', 'password').split('.'))

resource = configuration.services.provide_by_login_key.call(key)
return if resource.nil?

env['castle'].identify(resource.public_send(configuration.login['user_id']), {})

return unless configuration.services.validate_password.call(resource, pass)

verdict = castle_authenticate(req, env)

case verdict[:action]
when 'allow'
req.session['castle_user_id'] = env['castle'].user_id
when 'challenge'
redirect_result = configuration.challenge.call(req, resource)
configuration.logout.call(req, env)
redirect_result
when 'deny'
redirect_result = configuration.services.deny.call(req, resource)
configuration.services.logout.call(req, env)
redirect_result
end
end

def castle_authenticate(req, env)
authenticate(
Castle::Client.to_context(req),
Castle::Client.to_options(
user_id: env['castle'].user_id,
event: '$login.succeeded'
)
)
end

def login?(req)
req.path == configuration.login['path'] && req.request_method == configuration.login['method'] && req.form_data?
end

end
end
end
27 changes: 16 additions & 11 deletions lib/castle/middleware/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@
require 'yaml'

module Castle
module Middleware
class Middleware
# Configuration object for Middleware
class Configuration
extend Forwardable
attr_accessor :options, :events, :login
def_delegators :@options, :logger, :transport, :error_handler, :api_secret, :app_id, :deny, :challenge, :logout, :provide_by_id, :provide_by_login_key, :validate_password, :tracker_url
attr_reader :options
def_delegators :@options,
:logger, :transport, :api_secret, :app_id, :tracker_url, :services,
:events, :login
# :deny, :challenge, :logout, :provide_by_id, :provide_by_login_key, :validate_password
def_delegators :@middleware, :log

def initialize(options = nil)
self.options = options
@options = options
@middleware = Middleware.instance
setup
end

# Reset to default options
def setup
options.file_path ||= 'config/castle.yml'
options.transport = lambda do |context, options|
Castle::Middleware.track(context, options)
services.transport ||= lambda do |context, options|
Middleware.instance.track(context, options)
end
# Forward setting to Castle SDK
Castle.api_secret = api_secret
Expand All @@ -29,12 +34,12 @@ def setup

def load_config_file
file_config = YAML.load_file(options.file_path)
self.events = file_config['events'] || {}
self.login = file_config['login'] || {}
rescue Errno::ENOENT
Castle::Middleware.log(:error, '[Castle] No config file found')
options.events = (options.events || {}).merge(file_config['events'] || {})
options.login = (options.events || {}).merge(file_config['login'] || {})
rescue Errno::ENOENT => e
log(:error, '[Castle] No config file found')
rescue Psych::SyntaxError
Castle::Middleware.log(:error, '[Castle] Invalid YAML in config file')
log(:error, '[Castle] Invalid YAML in config file')
end
end
end
Expand Down
19 changes: 8 additions & 11 deletions lib/castle/middleware/configuration_options.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
# frozen_string_literal: true

module Castle
module Middleware
class Middleware
# Configuration options accessible for configure in mounted app

class ConfigurationOptions
%i[
api_secret
app_id
tracker_url
auto_insert_middleware
error_handler
file_path
logger
transport
deny
challenge
logout
provide_by_id
provide_by_login_key
validate_password
events
login
].each do |opt|
attr_accessor opt
end

attr_reader :services

def initialize
self.auto_insert_middleware = false
@services = ConfigurationServices.new
end
end
end
Expand Down
21 changes: 21 additions & 0 deletions lib/castle/middleware/configuration_services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Castle
class Middleware
# Configuration services (procs, lambdas) available to setup in configure block
class ConfigurationServices
%i[
error_handler
transport
deny
challenge
logout
provide_by_id
provide_by_login_key
validate_password
].each do |opt|
attr_accessor opt
end
end
end
end
2 changes: 1 addition & 1 deletion lib/castle/middleware/event_mapper.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Castle
module Middleware
class Middleware
# Map a request path to a Castle event name
class EventMapper
Object = Struct.new(:event, :method, :path, :status, :properties)
Expand Down
2 changes: 1 addition & 1 deletion lib/castle/middleware/params_flattener.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Castle
module Middleware
class Middleware
# Flatten nested Hashes
class ParamsFlattener
def self.call(object, prefix = nil)
Expand Down
19 changes: 8 additions & 11 deletions lib/castle/middleware/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
require 'rails/railtie'

module Castle
module Middleware
class Middleware
class Railtie < ::Rails::Railtie
initializer 'rollbar.middleware.rails' do |app|
# TODO(wallin): Flash middleware might not exist. Look for common
# Rack Middlewares instead?
# https://github.com/rails/rails/blob/ac3564693c6df9c9f9a46f681f4f6a4ea84997e6/guides/source/rails_on_rack.md#internal-middleware-stack
if Middleware.configuration.auto_insert_middleware
app.config.middleware.insert_after ActionDispatch::Flash,
Castle::Middleware::Tracking
app.config.middleware.insert_after ActionDispatch::Flash,
Castle::Middleware::Sensor
end
initializer 'castle.middleware.rails' do |app|
app.config.middleware.insert_after ActionDispatch::Flash,
Castle::Middleware::Authenticating
app.config.middleware.insert_after ActionDispatch::Flash,
Castle::Middleware::Tracking
app.config.middleware.insert_after ActionDispatch::Flash,
Castle::Middleware::Sensor
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/castle/middleware/request_config.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Castle
module Middleware
class Middleware
class RequestConfig
attr_reader :user_id
attr_reader :traits
Expand Down
21 changes: 21 additions & 0 deletions lib/castle/middleware/secure_headers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Castle
class Middleware
# Configuration services (procs, lambdas) available to setup in configure block
class SecureHeaders
def initialize
@can_append_nonce = ::SecureHeaders.respond_to?(:content_security_policy_script_nonce) &&
defined?(::SecureHeaders::Configuration) &&
!::SecureHeaders::Configuration.get.csp.opt_out? &&
!::SecureHeaders::Configuration.get.current_csp[:script_src].to_a.include?("'unsafe-inline'")
end

def call(env)
return unless @can_append_nonce
nonce = ::SecureHeaders.content_security_policy_script_nonce(::Rack::Request.new(env))
" nonce=\"#{nonce}\""
end
end
end
end
Loading

0 comments on commit fd76c9d

Please sign in to comment.