8000 Allow OIDC Providers to be available via local socket by jvanderhoof · Pull Request #2616 · cyberark/conjur · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Allow OIDC Providers to be available via local socket #2616

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

Draft
wants to merge 4 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
12 changes: 5 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Nothing should go in this section, please add to the latest unreleased version
(and update the corresponding date), or add a new version.

## [1.18.1] - 2022-08-01

## [1.18.0] - 2022-08-01

### Added
- Adds support for namespace label based identity scope for the Kubernetes Authenticator
[cyberark/conjur#2613](https://github.com/cyberark/conjur/pull/2613)
## [1.18.1] - 2022-07-29
### Changed
- Adds support for local processes to retrieve a list of configured OIDC providers
[cyberark/conjur#2616](https://github.com/cyberark/conjur/pull/2616)

## [1.18.0] - 2022-07-12
### Changed
- Adds support for authentication using OIDC's code authorization flow
[cyberark/conjur#2595](https://github.com/cyberark/conjur/pull/2595)
Expand Down
8 changes: 6 additions & 2 deletions app/db/repository/authenticator_repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ def initialize(data_object:, resource_repository: ::Resource, logger: Rails.logg
end

def find_all(type:, account:)
@resource_repository.where(
authenticators = @resource_repository.where(
Sequel.like(
:resource_id,
"#{account}:webservice:conjur/#{type}/%"
)
).all.map do |webservice|
)
@logger.debug("#{self.class}##{__method__} - #{authenticators.count} found")
return [] if authenticators.count.zero?

authenticators.all.map do |webservice|
load_authenticator(account: account, id: webservice.id.split(':').last, type: type)
end.compact
end
Expand Down
40 changes: 40 additions & 0 deletions app/domain/authentication/authn_oidc/v2/commands/list_providers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require 'command_class'
require './app/db/repository/authenticator_repository'

module Authentication
module AuthnOidc
module V2
module Commands
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authentication::AuthnOidc::V2::Commands has no descriptive comment

ListProviders ||= CommandClass.new(
dependencies: {
json_lib: JSON,
logger: Rails.logger
},
inputs: %i[message]
) do
def call
params = @json_lib.parse(@message)
@logger.debug("#{self.class}##{__method__} - #{params}")
(account = params.delete("account")) || raise("'account' is required")

authenticators = DB::Repository::AuthenticatorRepository.new(
data_object: Authentication::AuthnOidc::V2::DataObjects::Authenticator,
logger: @logger
).find_all(
account: account,
type: 'authn-oidc'
)
@logger.debug("#{self.class}##{__method__} - #{authenticators.inspect}")
Authentication::AuthnOidc::V2::Views::ProviderContext.new(
logger: @logger
).call(
authenticators: authenticators
)
end
end
end
end
end
end
29 changes: 29 additions & 0 deletions app/domain/commands/authentication/issue_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

require 'command_class'

module Commands
module Authentication
IssueToken ||= CommandClass.new(
dependencies: {
json_lib: JSON,
slosilo_lib: Slosilo
},
inputs: %i[message]
) do
def call
claims = @json_lib.parse(@message)
claims = claims.slice("account", "sub", "exp", "cidr")
(account = claims.delete("account")) || raise("'account' is required")
raise "'sub' is required" unless claims['sub']

key = @slosilo_lib["authn:#{account}"]
if key
key.issue_jwt(claims).to_json
else
raise "No signing key found for account #{account.inspect}"
end
end
end
end
end
77 changes: 77 additions & 0 deletions app/domain/util/socket_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require 'timeout'
require 'fileutils'
require 'socket'

module Util
class SocketService
def initialize(socket:, queue_length: 5, timeout: 1, logger: Logger.new($stderr, level: Logger::INFO))
@socket = socket
@queue_length = queue_length
@timeout = timeout
@logger = logger

validate_filesystem_assumptions(socket_file: socket)
end

def validate_filesystem_assumptions(socket_file:)
if File.exist?(socket_file)
raise "Socket: #{socket_file} already exists"
end

socket_dir = File.dirname(socket_file)

return if File.directory?(socket_dir)

raise("Socket Service requires directory #{socket_dir.inspect} to exist and be a directory")
end

def cleanup
# remove the socket on exit
# !! Note !!: A logger isn't allowed in a `trap` block, so we need
# to provide an IO.pipe to write the exit message to.
$stderr.puts("Removing socket #{@socket}")
File.unlink(@socket)
end

# Accepts a block for the desired response behavior.
# The message passed to the socket is available as a string
# inside the block as a block attribute. Ex:
#
# Util::SocketService.new(socket: '/socket/path').run do |passed_arguments|
# Authentication::AuthnOidc::V2::Commands::ListProviders.new.call(
# message: passed_arguments
# )
# end
#
# !! Note: the response is transformed into JSON to be sent through the socket.
def run(&block)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method run has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex method Util::SocketService#run (25.8)

server = UNIXServer.new(@socket)
trap(0) { cleanup }

server.listen(@queue_length)
@logger.info("service is listening at #{@socket}")

loop do
connection = server.accept
begin
Timeout.timeout(@timeout) do
arguments = connection.gets.strip
begin
@logger.debug("arguments: #{arguments.inspect}")
response = connection.puts(block.call(arguments).to_json)
@logger.debug("response: #{response.inspect}")
response
rescue
@logger.error("Error in service '#{@socket}': #{$ERROR_INFO}")
connection.puts
ensure
connection.close
end
end
rescue Timeout::Error
@logger.error("Timeout::Error in service '#{@socket}'")
end
end
end
end
end
62 changes: 8 additions & 54 deletions app/models/authn_local.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# frozen_string_literal: true

require 'timeout'
require 'fileutils'
require 'socket'

AuthnLocal = Struct.new(:socket, :queue_length, :timeout) do
class << self
def run socket:, queue_length:, timeout:
Expand All @@ -22,57 +18,15 @@ def run socket:, queue_length:, timeout:
timeout ||= 1
timeout = timeout.to_i

AuthnLocal.new(socket, queue_length, timeout).run
end
end

def run
FileUtils.rm_rf(socket)

server = UNIXServer.new(socket)

trap(0) do
# remove the socket on exit
# alternatively it can be removed on startup
# (or both)
$stderr.puts("Removing socket #{socket}")
File.unlink(socket)
end

server.listen(queue_length)

puts("authn-local is listening at #{socket}")

while conn = server.accept
begin
Timeout.timeout(timeout) do
claims = conn.gets.strip
begin
conn.puts(issue_token(claims))
rescue
$stderr.puts("Error in authn-local: #{$!.to_s}")
conn.puts
ensure
conn.close
end
end
rescue Timeout::Error
$stderr.puts("Timeout::Error in authn-local")
Util::SocketService.new(
socket: socket,
queue_length: queue_length,
timeout: timeout
).run do |passed_arguments|
Commands::Authentication::IssueToken.new.call(
message: passed_arguments
)
end
end
end

def issue_token claims
claims = JSON.parse(claims)
claims = claims.slice("account", "sub", "exp", "cidr")
(account = claims.delete("account")) || raise("'account' is required")
raise "'sub' is required" unless claims['sub']

key = Slosilo["authn:#{account}"]
if key
key.issue_jwt(claims).to_json
else
raise "No signing key found for account #{account.inspect}"
end
end
end
11 changes: 9 additions & 2 deletions bin/conjur-cli/commands/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def call
# processes
fork_server_process
fork_authn_local_process
fork_provider_list_process
fork_rotation_process

# Block until all child processes end
Expand Down Expand Up @@ -98,12 +99,12 @@ def load_bootstrap_policy
def cleanup_pidfile
# Get the path to conjurctl
conjurctl_path = `readlink -f $(which conjurctl)`

# Navigate from its directory (/bin) to the root Conjur server directory
conjur_server_dir = Pathname.new(File.join(File.dirname(conjurctl_path), '..')).cleanpath
pid_file_path = File.join(conjur_server_dir, 'tmp/pids/server.pid')
return unless File.exist?(pid_file_path)

puts("Removing existing PID file: #{pid_file_path}")
File.delete(pid_file_path)
end
Expand Down Expand Up @@ -133,6 +134,12 @@ def fork_authn_local_process
end
end

def fork_provider_list_process
Process.fork do
exec("rake ui:run")
end
end

def fork_rotation_process
# Only start the rotation watcher on leader, not on replicas
#
Expand Down
14 changes: 14 additions & 0 deletions lib/tasks/authn_local.rake
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,18 @@ namespace :authn_local do

AuthnLocal.run(socket: socket, queue_length: queue_length, timeout: timeout)
end

task authenticate: :environment do
require 'json'
require 'socket'

response = UNIXSocket.open('/run/authn-local/.socket') do |socket|
socket.puts({
account: 'cucumber',
sub: 'admin'
}.to_json)
JSON.parse(socket.gets)
end
puts("received: #{response}")
end
end
31 changes: 31 additions & 0 deletions lib/tasks/ui.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require './app/domain/util/socket_server'
require './app/domain/authentication/authn_oidc/v2/commands/list_providers'

namespace :ui do
task run: :environment do
logger = Logger.new($stderr, level: Logger::DEBUG)
Util::SocketService.new(
socket: '/run/ui/.socket',
logger: logger
).run do |passed_arguments|
Authentication::AuthnOidc::V2::Commands::ListProviders.new(
logger: logger
).call(
message: passed_arguments
)
end
end

task retrieve_providers: :environment do
require 'json'
require 'socket'

response = UNIXSocket.open('/run/ui/.socket') do |socket|
socket.puts({ account: 'cucumber' }.to_json)
JSON.parse(socket.gets)
end
puts("received: #{response}")
end
end
0