Skip to content

Commit 7f0cbde

Browse files
authored
Added basic authentication for LRS endpoint (#1038)
* Added basic authentication for LRS endpoint * rubo
1 parent b344dd9 commit 7f0cbde

File tree

5 files changed

+134
-56
lines changed

5 files changed

+134
-56
lines changed

app/models/tenant.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
class Tenant < ApplicationRedisRecord
44
SECRETS_SEPARATOR = ':'
55

6-
define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password
6+
define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :lrs_basic_token, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username,
7+
:kc_password
78

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

1718
# Custom LRS work
1819
application_redis_attr :lrs_endpoint
20+
application_redis_attr :lrs_basic_token
1921
application_redis_attr :kc_token_url
2022
application_redis_attr :kc_client_id
2123
application_redis_attr :kc_client_secret
@@ -43,6 +45,7 @@ def save!
4345
pipeline.hset(id_key, 'name', name) if name_changed?
4446
pipeline.hset(id_key, 'secrets', secrets) if secrets_changed?
4547
pipeline.hset(id_key, 'lrs_endpoint', lrs_endpoint) if lrs_endpoint_changed?
48+
pipeline.hset(id_key, 'lrs_basic_token', lrs_basic_token) if lrs_basic_token_changed?
4649
pipeline.hset(id_key, 'kc_token_url', kc_token_url) if kc_token_url_changed?
4750
pipeline.hset(id_key, 'kc_client_id', kc_client_id) if kc_client_id_changed?
4851
pipeline.hset(id_key, 'kc_client_secret', kc_client_secret) if kc_client_secret_changed?

app/services/lrs_payload_service.rb

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,16 @@ def initialize(tenant:, secret:)
77
end
88

99
def call
10-
Rails.logger.debug { "Fetching LRS token from #{@tenant.kc_token_url}" }
11-
12-
url = URI.parse(@tenant.kc_token_url)
13-
http = Net::HTTP.new(url.host, url.port)
14-
http.use_ssl = (url.scheme == 'https')
15-
16-
payload = {
17-
client_id: @tenant.kc_client_id,
18-
client_secret: @tenant.kc_client_secret,
19-
username: @tenant.kc_username,
20-
password: @tenant.kc_password,
21-
grant_type: 'password'
22-
}
10+
token = @tenant.kc_token_url.present? ? fetch_token_from_keycloak : @tenant.lrs_basic_token
2311

24-
request = Net::HTTP::Post.new(url.path)
25-
request.set_form_data(payload)
26-
27-
response = http.request(request)
28-
29-
if response.code.to_i != 200
30-
Rails.logger.warn("Error #{response.message} when trying to fetch LRS Access Token")
12+
if token.nil?
13+
Rails.logger.warn("LRS Token not found")
3114
return nil
3215
end
3316

34-
parsed_response = JSON.parse(response.body)
35-
kc_access_token = parsed_response['access_token']
36-
3717
lrs_payload = {
3818
lrs_endpoint: @tenant.lrs_endpoint,
39-
lrs_token: kc_access_token
19+
lrs_token: token
4020
}
4121

4222
# Generate a random salt
@@ -60,4 +40,35 @@ def call
6040

6141
nil
6242
end
43+
44+
private
45+
46+
def fetch_token_from_keycloak
47+
Rails.logger.debug { "Fetching LRS token from #{@tenant.kc_token_url}" }
48+
49+
url = URI.parse(@tenant.kc_token_url)
50+
http = Net::HTTP.new(url.host, url.port)
51+
http.use_ssl = (url.scheme == 'https')
52+
53+
payload = {
54+
client_id: @tenant.kc_client_id,
55+
client_secret: @tenant.kc_client_secret,
56+
username: @tenant.kc_username,
57+
password: @tenant.kc_password,
58+
grant_type: 'password'
59+
}
60+
61+
request = Net::HTTP::Post.new(url.path)
62+
request.set_form_data(payload)
63+
64+
response = http.request(request)
65+
66+
if response.code.to_i != 200
67+
Rails.logger.warn("Error #{response.message} when trying to fetch LRS Access Token")
68+
return nil
69+
end
70+
71+
parsed_response = JSON.parse(response.body)
72+
parsed_response['access_token']
73+
end
6374
end

lib/tasks/tenants.rake

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ task tenants: :environment do |_t, _args|
1515
puts("\tname: #{tenant.name}")
1616
puts("\tsecrets: #{tenant.secrets}")
1717
puts("\tlrs_endpoint: #{tenant.lrs_endpoint}") if tenant.lrs_endpoint.present?
18+
puts("\tlrs_basic_token: #{tenant.lrs_basic_token}") if tenant.lrs_basic_token.present?
1819
puts("\tkc_token_url: #{tenant.kc_token_url}") if tenant.kc_token_url.present?
1920
puts("\tkc_client_id: #{tenant.kc_client_id}") if tenant.kc_client_id.present?
2021
puts("\tkc_client_secret: #{tenant.kc_client_secret}") if tenant.kc_client_secret.present?
@@ -66,8 +67,30 @@ namespace :tenants do
6667
puts("Updated Tenant id: #{tenant.id}")
6768
end
6869

69-
desc 'Update an existing Tenants LRS credentials'
70-
task :update_lrs, [:id, :lrs_endpoint, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password] => :environment do |_t, args|
70+
desc 'Update an existing Tenants LRS credentials with basic authentication'
71+
task :update_lrs_basic, [:id, :lrs_endpoint, :lrs_basic_token] => :environment do |_t, args|
72+
check_multitenancy
73+
id = args[:id]
74+
lrs_endpoint = args[:lrs_endpoint]
75+
lrs_basic_token = args[:lrs_basic_token]
76+
77+
if id.blank? || lrs_endpoint.blank? || lrs_basic_token.blank?
78+
puts('Error: id, LRS_ENDPOINT, LRS_BASIC_TOKEN are required to update a Tenant')
79+
exit(1)
80+
end
81+
82+
tenant = Tenant.find(id)
83+
tenant.lrs_endpoint = lrs_endpoint
84+
tenant.lrs_basic_token = lrs_basic_token
85+
86+
tenant.save!
87+
88+
puts('OK')
89+
puts("Updated Tenant id: #{tenant.id}")
90+
end
91+
92+
desc 'Update an existing Tenants LRS credentials with Keycloak'
93+
task :update_lrs_kc, [:id, :lrs_endpoint, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password] => :environment do |_t, args|
7194
check_multitenancy
7295
id = args[:id]
7396
lrs_endpoint = args[:lrs_endpoint]
@@ -79,7 +102,7 @@ namespace :tenants do
79102

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

spec/factories/tenant.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
name { Faker::Creature::Animal.name }
66
secrets { "#{Faker::Crypto.sha256}:#{Faker::Crypto.sha512}" }
77
lrs_endpoint { nil }
8+
lrs_basic_token { nil }
89
kc_token_url { nil }
910
kc_client_id { nil }
1011
kc_client_secret { nil }

spec/services/lrs_payload_service_spec.rb

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,82 @@
33
require 'rails_helper'
44

55
RSpec.describe LrsPayloadService, type: :service do
6-
let!(:tenant) do
7-
create(:tenant,
8-
name: 'bn',
9-
lrs_endpoint: 'https://lrs_endpoint.com',
10-
kc_token_url: 'https://token_url.com/auth/token',
11-
kc_client_id: 'client_id',
12-
kc_client_secret: 'client_secret',
13-
kc_username: 'kc_username',
14-
kc_password: 'kc_password')
15-
end
16-
176
describe '#call' do
18-
it 'makes a call to kc_token_url with the correct payload' do
19-
payload = {
20-
client_id: tenant.kc_client_id,
21-
client_secret: tenant.kc_client_secret,
22-
username: tenant.kc_username,
23-
password: tenant.kc_password,
24-
grant_type: 'password'
25-
}
7+
context 'Basic Auth' do
8+
it 'uses the lrs_basic_token if set' do
9+
tenant = create(:tenant, name: 'bn', lrs_endpoint: 'https://lrs_endpoint.com', lrs_basic_token: 'basic_token')
10+
11+
encrypted_value = described_class.new(tenant: tenant, secret: 'server-secret').call
12+
13+
expect(JSON.parse(decrypt(encrypted_value, 'server-secret'))["lrs_token"]).to eq(tenant.lrs_basic_token)
14+
end
2615

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

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

32-
expect(stub_create).to have_been_requested
21+
expect(described_class.new(tenant: tenant, secret: 'server-secret').call).to be_nil
22+
end
3323
end
3424

35-
it 'logs a warning and returns nil if kc_token_url returns an error' do
36-
stub_request(:post, tenant.kc_token_url)
37-
.to_return(status: 500, body: 'Internal Server Error', headers: {})
25+
context 'Keycloak' do
26+
let!(:tenant) do
27+
create(:tenant,
28+
name: 'bn',
29+
lrs_endpoint: 'https://lrs_endpoint.com',
30+
kc_token_url: 'https://token_url.com/auth/token',
31+
kc_client_id: 'client_id',
32+
kc_client_secret: 'client_secret',
33+
kc_username: 'kc_username',
34+
kc_password: 'kc_password')
35+
end
36+
37+
it 'makes a call to kc_token_url with the correct payload' do
38+
payload = {
39+
client_id: tenant.kc_client_id,
40+
client_secret: tenant.kc_client_secret,
41+
username: tenant.kc_username,
42+
password: tenant.kc_password,
43+
grant_type: 'password'
44+
}
45+
46+
stub_create = stub_request(:post, tenant.kc_token_url)
47+
.with(body: payload).to_return(body: "kc_access_token")
48+
49+
described_class.new(tenant: tenant, secret: 'server-secret').call
50+
51+
expect(stub_create).to have_been_requested
52+
end
3853

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

41-
expect(described_class.new(tenant: tenant, secret: 'server-secret').call).to be_nil
58+
expect(Rails.logger).to receive(:warn).twice
59+
60+
expect(described_class.new(tenant: tenant, secret: 'server-secret').call).to be_nil
61+
end
4262
end
4363
end
64+
65+
private
66+
67+
def decrypt(encrypted_text, secret)
68+
decoded_text = Base64.strict_decode64(encrypted_text)
69+
70+
salt = decoded_text[8, 8]
71+
ciphertext = decoded_text[16..]
72+
73+
key_iv_bytes = OpenSSL::PKCS5.pbkdf2_hmac(secret, salt, 10_000, 48, 'sha256')
74+
key = key_iv_bytes[0, 32]
75+
iv = key_iv_bytes[32..]
76+
77+
decipher = OpenSSL::Cipher.new('aes-256-cbc')
78+
decipher.decrypt
79+
decipher.key = key
80+
decipher.iv = iv
81+
82+
decipher.update(ciphertext) + decipher.final
83+
end
4484
end

0 commit comments

Comments
 (0)