Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authenticator refactor v2 #2999

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ gem 'rack-rewrite'

gem 'dry-struct'
gem 'dry-types'
gem 'dry-validation'
gem 'net-ldap'

# for AWS rotator
Expand Down
19 changes: 19 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,26 @@ GEM
multi_json
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dry-configurable (1.1.0)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-core (1.0.0)
concurrent-ruby (~> 1.0)
zeitwerk (~> 2.6)
dry-inflector (1.0.0)
dry-initializer (3.1.1)
dry-logic (1.5.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-schema (1.13.3)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.0, < 2)
dry-initializer (~> 3.0)
dry-logic (>= 1.4, < 2)
dry-types (>= 1.7, < 2)
zeitwerk (~> 2.6)
dry-struct (1.6.0)
dry-core (~> 1.0, < 2)
dry-types (>= 1.7, < 2)
Expand All @@ -204,6 +216,12 @@ GEM
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
dry-validation (1.10.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
dry-initializer (~> 3.0)
dry-schema (>= 1.12, < 2)
zeitwerk (~> 2.6)
erubi (1.12.0)
event_emitter (0.2.6)
eventmachine (1.2.7)
Expand Down Expand Up @@ -542,6 +560,7 @@ DEPENDENCIES
debase-ruby_core_source (~> 3.2.1)
dry-struct
dry-types
dry-validation
event_emitter
faye-websocket
ffi (>= 1.9.24)
Expand Down
47 changes: 47 additions & 0 deletions app/controllers/authenticate_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ class AuthenticateController < ApplicationController
include BasicAuthenticator
include AuthorizeResource

def authenticate_via_get
handler = Authentication::Handler::AuthenticationHandler.new(
authenticator_type: params[:authenticator]
)

# Allow an authenticator to define the params it's expecting
response = handler.call(
parameters: params.permit(handler.params_allowed).to_h.symbolize_keys,
request_ip: request.ip
).bind do |auth_token|
return render_authn_token(auth_token)
end

error_response(response)
rescue => e
log_backtrace(e)
raise e
end

def authenticate_via_post
response = Authentication::Handler::AuthenticationHandler.new(
authenticator_type: params[:authenticator]
).call(
parameters: params,
request_body: request.body.read,
request_ip: request.ip
).bind do |auth_token|
return render_authn_token(auth_token)
end

error_response(response)
rescue => e
log_backtrace(e)
raise e
end

def error_response(response)
logger.info(LogMessages::Authentication::AuthenticationError.new(response.exception))
logger.info("Exception: #{response.exception.class.name}: #{response.exception.message}")
[*response.exception.backtrace].each { |line| logger.info(line) }

render(
json: { error: response.exception.message },
status: response.status
)
end

def oidc_authenticate_code_redirect
# TODO: need a mechanism for an authenticator strategy to define the required
# params. This will likely need to be done via the Handler.
Expand Down
26 changes: 20 additions & 6 deletions app/controllers/providers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@ def index
namespace = Authentication::Util::NamespaceSelector.select(
authenticator_type: params[:authenticator]
)
authenticator_klass = "#{namespace}::DataObjects::Authenticator".constantize
validations = "#{namespace}::Validations::AuthenticatorConfiguration".constantize.new(
utils: ::Util::ContractUtils
)
validator = DB::Validation.new(validations: validations)

authenticators = DB::Repository::AuthenticatorRepository.new.find_all(
account: params[:account],
type: params[:authenticator]
).bind do |authenticators_data|
authenticators_data.map do |authenticator_data|
# perform validation on each record
verified_data = validator.validate(data: authenticator_data)
if verified_data.success?
authenticator_klass.new(**verified_data.result)
end
end.compact
end

render(
json: "#{namespace}::Views::ProviderContext".constantize.new.call(
authenticators: DB::Repository::AuthenticatorRepository.new(
data_object: "#{namespace}::DataObjects::Authenticator".constantize
).find_all(
account: params[:account],
type: params[:authenticator]
)
authenticators: authenticators
)
)
end
Expand Down
117 changes: 75 additions & 42 deletions app/db/repository/authenticator_repository.rb
Original file line number Diff line number Diff line change
@@ -1,85 +1,118 @@
# frozen_string_literal: true

module DB
module Repository
# This class is responsible for loading the variables associated with a
# particular type of authenticator. Each authenticator requires a Data
# Object and Data Object Contract (for validation). Data Objects that
# fail validation are not returned.
#
# This class includes two public methods:
# - `find_all` - returns all available authenticators of a specified type
# from an account
# - `find` - returns a single authenticator based on the provided type,
# account, and service identifier.
#
class AuthenticatorRepository
def initialize(
data_object:,
resource_repository: ::Resource,
available_authenticators: Authentication::InstalledAuthenticators,
logger: Rails.logger
)
@resource_repository = resource_repository
@data_object = data_object
@available_authenticators = available_authenticators
@logger = logger

@success = ::SuccessResponse
@failure = ::FailureResponse
end

def find_all(type:, account:)
@resource_repository.where(
Sequel.like(
:resource_id,
"#{account}:webservice:conjur/#{type}/%"
)
).all.map do |webservice|
authenticators = authenticator_webservices(type: type, account: account).map do |webservice|
service_id = service_id_from_resource_id(webservice.id)

# Querying for the authenticator webservice above includes the webservices
# for the authenticator status. The filter below removes webservices that
# don't match the authenticator policy.
next unless webservice.id.split(':').last == "conjur/#{type}/#{service_id}"

load_authenticator(account: account, service_id: service_id, type: type)
begin
load_authenticator_variables(account: account, service_id: service_id, type: type)
rescue => e
@logger.info("failed to load #{type} authenticator '#{service_id}' do to validation failure: #{e.message}")
nil
end
end.compact
@success.new(authenticators)
end

def find(type:, account:, service_id:)
webservice = @resource_repository.where(
def find(type:, account:, service_id: nil, &block)
identifier = [type, service_id].compact.join('/')

webservice = @resource_repository.where(
Sequel.like(
:resource_id,
"#{account}:webservice:conjur/#{type}/#{service_id}"
"#{account}:webservice:conjur/#{identifier}"
)
).first
return unless webservice

load_authenticator(account: account, service_id: service_id, type: type)
end
unless webservice
return @failure.new(
"Failed to find a webservice: '#{account}:webservice:conjur/#{identifier}'",
exception: Errors::Authentication::Security::WebserviceNotFound.new(identifier, account)
)
end

def exists?(type:, account:, service_id:)
@resource_repository.with_pk("#{account}:webservice:conjur/#{type}/#{service_id}") != nil
begin
@success.new(
load_authenticator_variables(
account: account,
service_id: service_id,
type: type
)
)
rescue => e
@failure.new(
e.message,
exception: e,
level: :debug
)
end
end

private

def authenticator_webservices(type:, account:)
@resource_repository.where(
Sequel.like(
:resource_id,
"#{account}:webservice:conjur/#{type}/%"
)
).all.select do |webservice|
# Querying for the authenticator webservice above includes the webservices
# for the authenticator status. The filter below removes webservices that
# don't match the authenticator policy.
webservice.id.split(':').last.match?(%r{^conjur/#{type}/[\w\-_]+$})
end
end

def service_id_from_resource_id(id)
full_id = id.split(':').last
full_id.split('/')[2]
end

def load_authenticator(type:, account:, service_id:)
def load_authenticator_variables(type:, account:, service_id:)
identifier = [type, service_id].compact.join('/')
variables = @resource_repository.where(
Sequel.like(
:resource_id,
"#{account}:variable:conjur/#{type}/#{service_id}/%"
"#{account}:variable:conjur/#{identifier}/%"
)
).eager(:secrets).all

args_list = {}.tap do |args|
{}.tap do |args|
args[:account] = account
args[:service_id] = service_id
variables.each do |variable|
next unless variable.secret

args[variable.resource_id.split('/')[-1].underscore.to_sym] = variable.secret.value
# If variable exists but does not have a secret, set the value to an empty string.
# This is used downstream for validating if a variable has been set or not, and thus,
# what error to raise.
value = variable.secret ? variable.secret.value : ''
args[variable.resource_id.split('/')[-1].underscore.to_sym] = value
end
end

begin
allowed_args = %i[account service_id] +
@data_object.const_get(:REQUIRED_VARIABLES) +
@data_object.const_get(:OPTIONAL_VARIABLES)
args_list = args_list.select { |key, value| allowed_args.include?(key) && value.present? }
@data_object.new(**args_list)
rescue ArgumentError => e
@logger.debug("DB::Repository::AuthenticatorRepository.load_authenticator - exception: #{e}")
nil
end
end
end
end
Expand Down
Loading