Skip to content

Commit

Permalink
Added basic authentication for LRS endpoint (#1038)
Browse files Browse the repository at this point in the history
* Added basic authentication for LRS endpoint

* rubo
  • Loading branch information
farhatahmad committed Jun 7, 2024
1 parent 5e36183 commit ed3559a
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 56 deletions.
5 changes: 4 additions & 1 deletion app/models/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
class Tenant < ApplicationRedisRecord
SECRETS_SEPARATOR = ':'

define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password
define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :lrs_basic_token, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username,
:kc_password

# Unique ID for this tenant
application_redis_attr :id
Expand All @@ -16,6 +17,7 @@ class Tenant < ApplicationRedisRecord

# Custom LRS work
application_redis_attr :lrs_endpoint
application_redis_attr :lrs_basic_token
application_redis_attr :kc_token_url
application_redis_attr :kc_client_id
application_redis_attr :kc_client_secret
Expand Down Expand Up @@ -43,6 +45,7 @@ def save!
pipeline.hset(id_key, 'name', name) if name_changed?
pipeline.hset(id_key, 'secrets', secrets) if secrets_changed?
pipeline.hset(id_key, 'lrs_endpoint', lrs_endpoint) if lrs_endpoint_changed?
pipeline.hset(id_key, 'lrs_basic_token', lrs_basic_token) if lrs_basic_token_changed?
pipeline.hset(id_key, 'kc_token_url', kc_token_url) if kc_token_url_changed?
pipeline.hset(id_key, 'kc_client_id', kc_client_id) if kc_client_id_changed?
pipeline.hset(id_key, 'kc_client_secret', kc_client_secret) if kc_client_secret_changed?
Expand Down
59 changes: 35 additions & 24 deletions app/services/lrs_payload_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,16 @@ def initialize(tenant:, secret:)
end

def call
Rails.logger.debug { "Fetching LRS token from #{@tenant.kc_token_url}" }

url = URI.parse(@tenant.kc_token_url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')

payload = {
client_id: @tenant.kc_client_id,
client_secret: @tenant.kc_client_secret,
username: @tenant.kc_username,
password: @tenant.kc_password,
grant_type: 'password'
}
token = @tenant.kc_token_url.present? ? fetch_token_from_keycloak : @tenant.lrs_basic_token

request = Net::HTTP::Post.new(url.path)
request.set_form_data(payload)

response = http.request(request)

if response.code.to_i != 200
Rails.logger.warn("Error #{response.message} when trying to fetch LRS Access Token")
if token.nil?
Rails.logger.warn("LRS Token not found")
return nil
end

parsed_response = JSON.parse(response.body)
kc_access_token = parsed_response['access_token']

lrs_payload = {
lrs_endpoint: @tenant.lrs_endpoint,
lrs_token: kc_access_token
lrs_token: token
}

# Generate a random salt
Expand All @@ -60,4 +40,35 @@ def call

nil
end

private

def fetch_token_from_keycloak
Rails.logger.debug { "Fetching LRS token from #{@tenant.kc_token_url}" }

url = URI.parse(@tenant.kc_token_url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')

payload = {
client_id: @tenant.kc_client_id,
client_secret: @tenant.kc_client_secret,
username: @tenant.kc_username,
password: @tenant.kc_password,
grant_type: 'password'
}

request = Net::HTTP::Post.new(url.path)
request.set_form_data(payload)

response = http.request(request)

if response.code.to_i != 200
Rails.logger.warn("Error #{response.message} when trying to fetch LRS Access Token")
return nil
end

parsed_response = JSON.parse(response.body)
parsed_response['access_token']
end
end
29 changes: 26 additions & 3 deletions lib/tasks/tenants.rake
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ task tenants: :environment do |_t, _args|
puts("\tname: #{tenant.name}")
puts("\tsecrets: #{tenant.secrets}")
puts("\tlrs_endpoint: #{tenant.lrs_endpoint}") if tenant.lrs_endpoint.present?
puts("\tlrs_basic_token: #{tenant.lrs_basic_token}") if tenant.lrs_basic_token.present?
puts("\tkc_token_url: #{tenant.kc_token_url}") if tenant.kc_token_url.present?
puts("\tkc_client_id: #{tenant.kc_client_id}") if tenant.kc_client_id.present?
puts("\tkc_client_secret: #{tenant.kc_client_secret}") if tenant.kc_client_secret.present?
Expand Down Expand Up @@ -66,8 +67,30 @@ namespace :tenants do
puts("Updated Tenant id: #{tenant.id}")
end

desc 'Update an existing Tenants LRS credentials'
task :update_lrs, [:id, :lrs_endpoint, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password] => :environment do |_t, args|
desc 'Update an existing Tenants LRS credentials with basic authentication'
task :update_lrs_basic, [:id, :lrs_endpoint, :lrs_basic_token] => :environment do |_t, args|
check_multitenancy
id = args[:id]
lrs_endpoint = args[:lrs_endpoint]
lrs_basic_token = args[:lrs_basic_token]

if id.blank? || lrs_endpoint.blank? || lrs_basic_token.blank?
puts('Error: id, LRS_ENDPOINT, LRS_BASIC_TOKEN are required to update a Tenant')
exit(1)
end

tenant = Tenant.find(id)
tenant.lrs_endpoint = lrs_endpoint
tenant.lrs_basic_token = lrs_basic_token

tenant.save!

puts('OK')
puts("Updated Tenant id: #{tenant.id}")
end

desc 'Update an existing Tenants LRS credentials with Keycloak'
task :update_lrs_kc, [:id, :lrs_endpoint, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password] => :environment do |_t, args|
check_multitenancy
id = args[:id]
lrs_endpoint = args[:lrs_endpoint]
Expand All @@ -79,7 +102,7 @@ namespace :tenants do

if id.blank? || lrs_endpoint.blank? || kc_token_url.blank? || kc_client_id.blank? ||
kc_client_secret.blank? || kc_username.blank? || kc_password.blank?
puts('Error: id and either name or secrets are required to update a Tenant')
puts('Error: LRS_ENDPOINT, KC_TOKEN_URL, KC_CLIENT_ID, KC_CLIENT_SECRET, KC_USERNAME, KC_PASSWORD are required to update a Tenant')
exit(1)
end

Expand Down
1 change: 1 addition & 0 deletions spec/factories/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
name { Faker::Creature::Animal.name }
secrets { "#{Faker::Crypto.sha256}:#{Faker::Crypto.sha512}" }
lrs_endpoint { nil }
lrs_basic_token { nil }
kc_token_url { nil }
kc_client_id { nil }
kc_client_secret { nil }
Expand Down
96 changes: 68 additions & 28 deletions spec/services/lrs_payload_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,82 @@
require 'rails_helper'

RSpec.describe LrsPayloadService, type: :service do
let!(:tenant) do
create(:tenant,
name: 'bn',
lrs_endpoint: 'https://lrs_endpoint.com',
kc_token_url: 'https://token_url.com/auth/token',
kc_client_id: 'client_id',
kc_client_secret: 'client_secret',
kc_username: 'kc_username',
kc_password: 'kc_password')
end

describe '#call' do
it 'makes a call to kc_token_url with the correct payload' do
payload = {
client_id: tenant.kc_client_id,
client_secret: tenant.kc_client_secret,
username: tenant.kc_username,
password: tenant.kc_password,
grant_type: 'password'
}
context 'Basic Auth' do
it 'uses the lrs_basic_token if set' do
tenant = create(:tenant, name: 'bn', lrs_endpoint: 'https://lrs_endpoint.com', lrs_basic_token: 'basic_token')

encrypted_value = described_class.new(tenant: tenant, secret: 'server-secret').call

expect(JSON.parse(decrypt(encrypted_value, 'server-secret'))["lrs_token"]).to eq(tenant.lrs_basic_token)
end

stub_create = stub_request(:post, tenant.kc_token_url)
.with(body: payload).to_return(body: "kc_access_token")
it 'logs a warning and returns nil if lrs_basic_token is not set' do
tenant = create(:tenant, name: 'bn', lrs_endpoint: 'https://lrs_endpoint.com')

described_class.new(tenant: tenant, secret: 'server-secret').call
expect(Rails.logger).to receive(:warn)

expect(stub_create).to have_been_requested
expect(described_class.new(tenant: tenant, secret: 'server-secret').call).to be_nil
end
end

it 'logs a warning and returns nil if kc_token_url returns an error' do
stub_request(:post, tenant.kc_token_url)
.to_return(status: 500, body: 'Internal Server Error', headers: {})
context 'Keycloak' do
let!(:tenant) do
create(:tenant,
name: 'bn',
lrs_endpoint: 'https://lrs_endpoint.com',
kc_token_url: 'https://token_url.com/auth/token',
kc_client_id: 'client_id',
kc_client_secret: 'client_secret',
kc_username: 'kc_username',
kc_password: 'kc_password')
end

it 'makes a call to kc_token_url with the correct payload' do
payload = {
client_id: tenant.kc_client_id,
client_secret: tenant.kc_client_secret,
username: tenant.kc_username,
password: tenant.kc_password,
grant_type: 'password'
}

stub_create = stub_request(:post, tenant.kc_token_url)
.with(body: payload).to_return(body: "kc_access_token")

described_class.new(tenant: tenant, secret: 'server-secret').call

expect(stub_create).to have_been_requested
end

expect(Rails.logger).to receive(:warn)
it 'logs a warning and returns nil if kc_token_url returns an error' do
stub_request(:post, tenant.kc_token_url)
.to_return(status: 500, body: 'Internal Server Error', headers: {})

expect(described_class.new(tenant: tenant, secret: 'server-secret').call).to be_nil
expect(Rails.logger).to receive(:warn).twice

expect(described_class.new(tenant: tenant, secret: 'server-secret').call).to be_nil
end
end
end

private

def decrypt(encrypted_text, secret)
decoded_text = Base64.strict_decode64(encrypted_text)

salt = decoded_text[8, 8]
ciphertext = decoded_text[16..]

key_iv_bytes = OpenSSL::PKCS5.pbkdf2_hmac(secret, salt, 10_000, 48, 'sha256')
key = key_iv_bytes[0, 32]
iv = key_iv_bytes[32..]

decipher = OpenSSL::Cipher.new('aes-256-cbc')
decipher.decrypt
decipher.key = key
decipher.iv = iv

decipher.update(ciphertext) + decipher.final
end
end

0 comments on commit ed3559a

Please sign in to comment.