Project

General

Profile

Download (4.97 KB) Statistics
| Branch: | Tag: | Revision:
require 'resolv'
require 'uri'

module Foreman::Controller::SmartProxyAuth
extend ActiveSupport::Concern

module ClassMethods
def add_smart_proxy_filters(actions, options = {})
skip_before_action :require_login, :only => actions, :raise => false
skip_before_action :authorize, :only => actions
skip_before_action :verify_authenticity_token, :only => actions
skip_before_action :set_taxonomy, :only => actions, :raise => false
skip_before_action :session_expiry, :update_activity_time, :only => actions
before_action(:only => actions) { require_smart_proxy_or_login(options[:features]) }
attr_reader :detected_proxy

cattr_accessor :smart_proxy_filter_actions
self.smart_proxy_filter_actions ||= []
self.smart_proxy_filter_actions.push(*actions)

prepend SmartProxyRequireSsl
end
end

module SmartProxyRequireSsl
def require_ssl?
if [self.smart_proxy_filter_actions].flatten.map(&:to_s).include?(self.action_name)
false
else
super
end
end
end

private

# Permits registered Smart Proxies or a user with permission
def require_smart_proxy_or_login(features = nil)
features = features.call if features.respond_to?(:call)
allowed_smart_proxies = if features.blank?
SmartProxy.unscoped.all
else
SmartProxy.unscoped.with_features(*features)
end

if !Setting[:restrict_registered_smart_proxies] || auth_smart_proxy(allowed_smart_proxies, Setting[:require_ssl_smart_proxies])
set_admin_user
return true
end

require_login
unless User.current
render_error 'access_denied', :status => :forbidden unless performed? && api_request?
return false
end
authorize
end

# Filter requests to only permit from hosts with a registered smart proxy
# Uses rDNS of the request to match proxy hostnames
def auth_smart_proxy(proxies = SmartProxy.unscoped.all, require_cert = true)
request_hosts = nil
if request.ssl?
# If we have the client certficate in the request environment we can extract the dn and sans from there
# if not we use the dn in the request environment
# SAN validation requires "SSLOptions +ExportCertData" in Apache httpd
if request.env.has_key?(Setting[:ssl_client_cert_env]) && request.env[Setting[:ssl_client_cert_env]].present? && request.env[Setting[:ssl_client_cert_env]] != '(null)'
logger.debug "Examining client certificate to extract dn and sans"
cert_raw = request.env[Setting[:ssl_client_cert_env]]
certificate = CertificateExtract.new(cert_raw)
logger.debug "Client sent certificate with subject '#{certificate.subject}' and subject alt names '#{certificate.subject_alternative_names.inspect}'"
else
dn = request.env[Setting[:ssl_client_dn_env]]
end

if (dn && dn =~ /CN=([^\s\/,]+)/i) || certificate
verify = request.env[Setting[:ssl_client_verify_env]]
if verify == 'SUCCESS'
# If the client sent certificate contains a subject or sans, use them for request_hosts, else fall back to the dn set in the request environment
request_hosts = []
if certificate
if certificate.subject_alternative_names.present?
request_hosts += certificate.subject_alternative_names
elsif certificate.subject
request_hosts << certificate.subject
end
else
request_hosts << Regexp.last_match(1) if Regexp.last_match(1)
end
else
logger.warn "SSL cert has not been verified (#{verify}) - request from #{request.ip}, #{dn}"
end
elsif require_cert
logger.warn "No SSL cert with CN supplied - request from #{request.ip}, #{dn}"
else
request_hosts = Resolv.new.getnames(request.ip)
end
elsif SETTINGS[:require_ssl]
logger.warn "SSL is required - request from #{request.ip}"
else
request_hosts = Resolv.new.getnames(request.ip)
request_hosts = [request.ip] if request_hosts == []
end
return false unless request_hosts

hosts = Hash[proxies.map { |p| [URI.parse(p.url).host, p] }]
allowed_hosts = hosts.keys.push(*Setting[:trusted_hosts])
logger.debug { "Verifying request from #{request_hosts.inspect} against #{allowed_hosts.inspect}" }

if (host = detect_matching_host(allowed_hosts, request_hosts))
@detected_proxy = hosts[host] if host
true
else
logger.warn "No smart proxy server found on #{request_hosts.inspect} and is not in trusted_hosts"
false
end
end

def detect_matching_host(allowed_hosts, request_hosts)
allowed_hosts.product(request_hosts).each do |allowed, request|
if request.starts_with?('*')
rex = /\A#{Regexp.escape(request).sub('\\*', '.*')}\Z/
return allowed if allowed =~ rex
else
return allowed if allowed == request
end
end

nil
end
end
(22-22/30)