Skip to content

Commit

Permalink
poc
Browse files Browse the repository at this point in the history
  • Loading branch information
philippthun committed Jul 7, 2023
1 parent 7506b17 commit 6d62b61
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 139 deletions.
3 changes: 3 additions & 0 deletions lib/cloud_controller/config_schemas/base/api_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ class ApiSchema < VCAP::Config
optional(:uaa_client_secret) => String,
optional(:uaa_client_scope) => String,

optional(:cc_zone_lookup_client_name) => String,
optional(:cc_zone_lookup_client_secret) => String,

cloud_controller_username_lookup_client_name: String,
cloud_controller_username_lookup_client_secret: String,

Expand Down
16 changes: 16 additions & 0 deletions lib/cloud_controller/dependency_locator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,23 @@ def router_group_type_populating_collection_renderer
create_paginated_collection_renderer(collection_transformer: RouterGroupTypePopulator.new(routing_api_client))
end

def uaa_zone_lookup_client
unless config.get(:cc_zone_lookup_client_name).nil?
# TODO: [UAA ZONES] Cache client?
UaaClient.new(
uaa_target: config.get(:uaa, :internal_url),
client_id: config.get(:cc_zone_lookup_client_name),
secret: config.get(:cc_zone_lookup_client_secret),
ca_file: config.get(:uaa, :ca_file),
)
end
end

def uaa_username_lookup_client
# TODO: [UAA ZONES] Cache client per zone id? Is subdomain change allowed/supported?
UaaClient.new(
uaa_target: config.get(:uaa, :internal_url),
zone: UaaZones.get_subdomain(uaa_zone_lookup_client, VCAP::CloudController::SecurityContext.zone_id),
client_id: config.get(:cloud_controller_username_lookup_client_name),
secret: config.get(:cloud_controller_username_lookup_client_secret),
ca_file: config.get(:uaa, :ca_file),
Expand All @@ -286,6 +300,7 @@ def uaa_username_lookup_client
def routing_api_client
return RoutingApi::DisabledClient.new if config.get(:routing_api).nil?

# TODO: [UAA ZONES] Is this use case relevant for zones?
uaa_client = UaaClient.new(
uaa_target: config.get(:uaa, :internal_url),
client_id: config.get(:routing_api, :routing_client_name),
Expand All @@ -299,6 +314,7 @@ def routing_api_client
end

def credhub_client
# TODO: [UAA ZONES] Is this use case relevant for zones?
uaa_client = UaaClient.new(
uaa_target: config.get(:uaa, :internal_url),
client_id: config.get(:cc_service_key_client_name),
Expand Down
4 changes: 4 additions & 0 deletions lib/cloud_controller/security_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,9 @@ def self.issuer

''
end

def self.zone_id
token['zid'] if valid_token?
end
end
end
92 changes: 74 additions & 18 deletions lib/cloud_controller/uaa/uaa_client.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,60 @@
module VCAP::CloudController
class UaaHttpClient
include CF::UAA::Http

def initialize(target, auth_header, options={})
@target = target
@auth_header = auth_header
initialize_http_options(options)
end

def get(path)
json_get(@target, path, nil, headers)
end

private

def headers
@auth_header.empty? ? {} : { 'authorization' => @auth_header }
end
end

class UaaClient
attr_reader :uaa_target, :client_id, :secret, :ca_file, :http_timeout
attr_reader :subdomain, :zone, :client_id, :secret, :ca_file, :http_timeout

def self.default_http_timeout
@default_http_timeout ||= VCAP::CloudController::Config.config.get(:uaa, :client_timeout)
end

def auth_header
token = UaaTokenCache.get_token(client_id)
return token if token
return '' if client_id.empty?

UaaTokenCache.set_token(client_id, token_info.auth_header, expires_in: token_info.info['expires_in'])
# TODO: [UAA ZONES] Cache token per client_id + subdomain.
# token = UaaTokenCache.get_token(client_id)
# return token if token
#
# UaaTokenCache.set_token(client_id, token_info.auth_header, expires_in: token_info.info['expires_in'])
token_info.auth_header
end

def initialize(uaa_target:, client_id:, secret:, ca_file:)
def initialize(uaa_target:, subdomain: '', zone: '', client_id: '', secret: '', ca_file:)
@uaa_target = uaa_target
@subdomain = subdomain
@zone = zone
@client_id = client_id
@secret = secret
@ca_file = ca_file
@http_timeout = self.class.default_http_timeout
end

def uaa_target
return @uaa_target if subdomain.empty?

uri = Addressable::URI.parse(@uaa_target)
uri.host = "#{subdomain}.#{uri.host}"
uri.to_s
end

def get_clients(client_ids)
client_ids.map do |id|
get(:client, id)
Expand Down Expand Up @@ -54,7 +87,8 @@ def usernames_for_ids(user_ids)
def id_for_username(username, origin: nil)
filter_string = %(username eq "#{username}")
filter_string = %/origin eq "#{origin}" and #{filter_string}/ if origin.present?
results = query(:user_id, includeInactive: true, filter: filter_string)
# TODO: [UAA ZONES] Is the changed query semantically identical?
results = query(:user, filter: filter_string, sort_by: 'username', attributes: 'id')

user = results['resources'].first
user && user['id']
Expand All @@ -69,12 +103,8 @@ def ids_for_usernames_and_origins(usernames, origins, precise_username_match=tru
origin_filter_string = origins&.map { |o| "origin eq \"#{o}\"" }&.join(' or ')

filter_string = construct_filter_string(username_filter_string, origin_filter_string)

if precise_username_match
results = query(:user_id, includeInactive: true, filter: filter_string)
else
results = query(:user, filter: filter_string, attributes: 'id')
end
# TODO: [UAA ZONES] Is the changed query semantically identical?
results = query(:user, filter: filter_string, sort_by: 'username', attributes: 'id')

results['resources'].map { |r| r['id'] }
rescue CF::UAA::UAAError => e
Expand All @@ -92,7 +122,8 @@ def construct_filter_string(username_filter_string, origin_filter_string)

def origins_for_username(username)
filter_string = %(username eq "#{username}")
results = query(:user_id, includeInactive: true, filter: filter_string)
# TODO: [UAA ZONES] Is the changed query semantically identical?
results = query(:user, filter: filter_string, sort_by: 'username', attributes: 'id,origin')

results['resources'].map { |resource| resource['origin'] }
rescue UaaUnavailable, CF::UAA::UAAError => e
Expand All @@ -104,6 +135,10 @@ def info
CF::UAA::Info.new(uaa_target, uaa_connection_opts)
end

def http_get(path)
http_client.get(path)
end

private

def query(type, **opts)
Expand All @@ -121,18 +156,16 @@ def with_cache_retry
yield
end

def scim
CF::UAA::Scim.new(uaa_target, auth_header, uaa_connection_opts)
end

def fetch_users(user_ids)
return {} unless user_ids.present?

results_hash = {}

user_ids.each_slice(200) do |batch|
filter_string = batch.map { |user_id| %(id eq "#{user_id}") }.join(' or ')
results = query(:user_id, filter: filter_string, count: batch.length)
filter_string = %/active eq true and ( #{filter_string} )/
# TODO: [UAA ZONES] Is the changed query semantically identical?
results = query(:user, filter: filter_string, count: batch.length, sort_by: 'username', attributes: 'id,username,origin')
results['resources'].each do |user|
results_hash[user['id']] = user
end
Expand All @@ -141,10 +174,22 @@ def fetch_users(user_ids)
results_hash
end

def scim
opts = uaa_connection_opts
opts.merge!({ zone: zone }) unless zone.empty?
CF::UAA::Scim.new(uaa_target, auth_header, opts)
end

def token_issuer
raise ArgumentError.new('TokenIssuer requires client_id') if client_id.empty?

CF::UAA::TokenIssuer.new(uaa_target, client_id, secret, uaa_connection_opts)
end

def http_client
UaaHttpClient.new(uaa_target, auth_header, uaa_connection_opts)
end

def uaa_connection_opts
{
skip_ssl_validation: false,
Expand All @@ -157,4 +202,15 @@ def logger
@logger ||= Steno.logger('cc.uaa_client')
end
end

class UaaZones
def self.get_subdomain(uaa_client, zone_id)
return '' if uaa_client.nil? || zone_id.nil?

zone = uaa_client.http_get("/identity-zones/#{zone_id}")
zone['subdomain']
rescue CF::UAA::NotFound
raise 'invalid zone id'
end
end
end
58 changes: 29 additions & 29 deletions lib/cloud_controller/uaa/uaa_token_decoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ def decode_token_with_asymmetric_key(auth_token)
tries -= 1
# If we uncover issues due to attempting to decode with every
# key, we can revisit: https://www.pivotaltracker.com/story/show/132270761
asymmetric_key.value.each do |key|
asymmetric_key(decode_token_zone_id(auth_token)).value.each do |key|
return decode_token_with_key(auth_token, pkey: key)
rescue CF::UAA::InvalidSignature => e
last_error = e
end
asymmetric_key.refresh
# asymmetric_key.refresh - TODO: [UAA ZONES] Enable once keys are cached.
end
raise last_error
end
Expand All @@ -90,14 +90,21 @@ def decode_token_with_key(auth_token, options)

raise BadToken.new('Incorrect token') unless access_token?(token)

if token['iss'] != uaa_issuer
@uaa_issuer = nil
raise BadToken.new('Incorrect issuer') if token['iss'] != uaa_issuer
if token['iss'] != uaa_issuer(token['zid'])
# TODO: [UAA ZONES] Clear cached issuer for this zone id.
raise BadToken.new('Incorrect issuer') # if token['iss'] != uaa_issuer(token['zid']) - TODO: [UAA ZONES] Enable once issuers are cached.
end

token
end

def decode_token_zone_id(token)
segments = token.split('.')
raise CF::UAA::InvalidTokenFormat.new('Not enough or too many segments') if segments.length < 2 || segments.length > 3

CF::UAA::Util.json_decode64(segments[1], :sym)[:zid]
end

def symmetric_key
config[:symmetric_secret]
end
Expand All @@ -106,37 +113,30 @@ def symmetric_key2
config[:symmetric_secret2]
end

def asymmetric_key
@asymmetric_key ||= UaaVerificationKeys.new(uaa_username_lookup_client.info)
end

def uaa_username_lookup_client
::CloudController::DependencyLocator.instance.uaa_username_lookup_client
def asymmetric_key(zone_id)
# TODO: [UAA ZONES] Cache keys per zone id.
UaaVerificationKeys.new(uaa_client(zone_id).info)
end

def uaa_issuer
@uaa_issuer ||= with_request_error_handling do
fetch_uaa_issuer
def uaa_issuer(zone_id)
# TODO: [UAA ZONES] Cache issuer per zone id.
with_request_error_handling do
fetch_uaa_issuer(zone_id)
end
end

def fetch_uaa_issuer
response = http_client.get('.well-known/openid-configuration')
raise "Could not retrieve issuer information from UAA: #{response.status}" unless response.status == 200

JSON.parse(response.body).fetch('issuer')
def fetch_uaa_issuer(zone_id)
uaa_client(zone_id).http_get('/.well-known/openid-configuration')['issuer']
rescue CF::UAA::UAAError
raise 'Could not retrieve issuer information from UAA'
end

def http_client
uaa_target = config[:internal_url]
uaa_ca = config[:ca_file]
client = HTTPClient.new(base_url: uaa_target)
client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
if !uaa_ca.nil? && !uaa_ca.empty?
client.ssl_config.set_trust_ca(uaa_ca)
end

client
def uaa_client(zone_id)
UaaClient.new(
uaa_target: config[:internal_url],
subdomain: UaaZones.get_subdomain(CloudController::DependencyLocator.instance.uaa_zone_lookup_client, zone_id),
ca_file: config[:ca_file],
)
end

def with_request_error_handling(&blk)
Expand Down
1 change: 1 addition & 0 deletions lib/services/sso/uaa/uaa_client_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def filter_uaa_client_scope
end

def create_uaa_client
# TODO: [UAA ZONES] Is this use case relevant for zones?
VCAP::CloudController::UaaClient.new(
uaa_target: uaa_target,
client_id: VCAP::CloudController::Config.config.get(:uaa_client_name),
Expand Down
29 changes: 20 additions & 9 deletions spec/unit/lib/cloud_controller/dependency_locator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -294,25 +294,36 @@
end

describe '#uaa_username_lookup_client' do
context 'when a CA file is not configured for the UAA' do
before do
TestConfig.override(uaa: { ca_file: nil, internal_url: TestConfig.config_instance.get(:uaa, :internal_url) })
end

context 'when a CA file is configured for the UAA' do
it 'returns a uaa client with credentials for looking up usernames' do
uaa_username_lookup_client = locator.uaa_username_lookup_client
expect(uaa_username_lookup_client.client_id).to eq(config.get(:cloud_controller_username_lookup_client_name))
expect(uaa_username_lookup_client.secret).to eq(config.get(:cloud_controller_username_lookup_client_secret))
expect(uaa_username_lookup_client.uaa_target).to eq(config.get(:uaa, :internal_url))
expect(uaa_username_lookup_client.ca_file).to eq(config.get(:uaa, :ca_file))
expect(uaa_username_lookup_client.zone).to eq('')
end
end

context 'when a CA file is configured for the UAA' do
context 'when a CA file is not configured for the UAA' do
before do
TestConfig.override(uaa: { ca_file: nil, internal_url: config.get(:uaa, :internal_url) })
end

it 'returns a uaa client with credentials for looking up usernames' do
uaa_username_lookup_client = locator.uaa_username_lookup_client
expect(uaa_username_lookup_client.client_id).to eq(config.get(:cloud_controller_username_lookup_client_name))
expect(uaa_username_lookup_client.secret).to eq(config.get(:cloud_controller_username_lookup_client_secret))
expect(uaa_username_lookup_client.uaa_target).to eq(config.get(:uaa, :internal_url))
expect(uaa_username_lookup_client.ca_file).to be_nil
end
end

context 'when a UAA zone is used' do
before do
allow(VCAP::CloudController::UaaZones).to receive(:get_subdomain).and_return('zone')
end

it 'adapts the UAA URL accordingly' do
uaa_username_lookup_client = locator.uaa_username_lookup_client
expect(uaa_username_lookup_client.zone).to eq('zone')
end
end
end
Expand Down
Loading

0 comments on commit 6d62b61

Please sign in to comment.