diff --git a/app/models/tenant.rb b/app/models/tenant.rb index 8ee40cdf..4244b8cf 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -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 @@ -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 @@ -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? diff --git a/app/services/lrs_payload_service.rb b/app/services/lrs_payload_service.rb index 202e8099..1233dded 100644 --- a/app/services/lrs_payload_service.rb +++ b/app/services/lrs_payload_service.rb @@ -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 @@ -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 diff --git a/lib/tasks/tenants.rake b/lib/tasks/tenants.rake index 5af6167b..91563b66 100644 --- a/lib/tasks/tenants.rake +++ b/lib/tasks/tenants.rake @@ -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? @@ -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] @@ -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 diff --git a/spec/factories/tenant.rb b/spec/factories/tenant.rb index 15f43012..7af50c8e 100644 --- a/spec/factories/tenant.rb +++ b/spec/factories/tenant.rb @@ -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 } diff --git a/spec/services/lrs_payload_service_spec.rb b/spec/services/lrs_payload_service_spec.rb index 841f47a5..bbf53cf2 100644 --- a/spec/services/lrs_payload_service_spec.rb +++ b/spec/services/lrs_payload_service_spec.rb @@ -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