Skip to content

Commit

Permalink
Initial work for custom lrs integration (#1035)
Browse files Browse the repository at this point in the history
* Initial work for custom lrs integration

* Added tests
  • Loading branch information
farhatahmad committed Jun 7, 2024
1 parent 6116ab5 commit 5e36183
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Metrics/PerceivedComplexity:
Exclude:
- app/models/recording.rb
- app/models/server.rb
- app/models/tenant.rb
- lib/server_sync.rb

# Avoid classes longer than 100 lines of code.
Expand Down Expand Up @@ -164,6 +165,7 @@ Metrics/CyclomaticComplexity:
Exclude:
- app/models/recording.rb
- app/models/server.rb
- app/models/tenant.rb
- lib/server_sync.rb

# Checks for method parameter names that contain capital letters, end in numbers, or do not meet a minimal length.
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/bigbluebutton_api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ def create

params[:voiceBridge] = meeting.voice_bridge

if @tenant&.lrs_endpoint.present?
lrs_payload = LrsPayloadService.new(tenant: @tenant, secret: server.secret).call
params[:'meta_secret-lrs-payload'] = lrs_payload if lrs_payload.present?
end

have_preuploaded_slide = request.post? && request.content_mime_type == Mime[:xml]

logger.debug("Creating meeting #{params[:meetingID]} on BigBlueButton server #{server.id}")
Expand Down
16 changes: 15 additions & 1 deletion app/models/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class Tenant < ApplicationRedisRecord
SECRETS_SEPARATOR = ':'

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

# Unique ID for this tenant
application_redis_attr :id
Expand All @@ -14,6 +14,14 @@ class Tenant < ApplicationRedisRecord
# Shared secrets for making API requests for this tenant (: separated)
application_redis_attr :secrets

# Custom LRS work
application_redis_attr :lrs_endpoint
application_redis_attr :kc_token_url
application_redis_attr :kc_client_id
application_redis_attr :kc_client_secret
application_redis_attr :kc_username
application_redis_attr :kc_password

def save!
with_connection do |redis|
raise RecordNotSaved.new('Cannot update id field', self) if id_changed? && !@new_record
Expand All @@ -34,6 +42,12 @@ def save!
pipeline.del(old_names_key) if !id_changed? && name_changed? # Delete the old name key if it's not a new record and the name was updated
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, '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?
pipeline.hset(id_key, 'kc_username', kc_username) if kc_username_changed?
pipeline.hset(id_key, 'kc_password', kc_password) if kc_password_changed?
pipeline.sadd?('tenants', id) if id_changed?
end
end
Expand Down
63 changes: 63 additions & 0 deletions app/services/lrs_payload_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

class LrsPayloadService
def initialize(tenant:, secret:)
@tenant = tenant
@secret = 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'
}

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)
kc_access_token = parsed_response['access_token']

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

# Generate a random salt
salt = SecureRandom.random_bytes(8)

# Generate a key and initialization vector (IV) using PBKDF2 with SHA-256
key_iv = OpenSSL::PKCS5.pbkdf2_hmac(@secret, salt, 10_000, 48, OpenSSL::Digest.new('SHA256'))
key = key_iv[0, 32] # 32 bytes for the key
iv = key_iv[32, 16] # 16 bytes for the IV

# Encrypt the data using AES-256-CBC
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
cipher.iv = iv

# Encrypt and Base64 encode the data
Base64.strict_encode64(Random.random_bytes(8) + salt + cipher.update(lrs_payload.to_json) + cipher.final)
rescue StandardError => e
Rails.logger.warn("Error #{e} when trying to compute LRS Payload")

nil
end
end
38 changes: 38 additions & 0 deletions lib/tasks/tenants.rake
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ task tenants: :environment do |_t, _args|
puts("id: #{tenant.id}")
puts("\tname: #{tenant.name}")
puts("\tsecrets: #{tenant.secrets}")
puts("\tlrs_endpoint: #{tenant.lrs_endpoint}") if tenant.lrs_endpoint.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?
puts("\tkc_username: #{tenant.kc_username}") if tenant.kc_username.present?
puts("\tkc_password: #{tenant.kc_password}") if tenant.kc_password.present?
end
end

Expand Down Expand Up @@ -53,6 +59,38 @@ namespace :tenants do
tenant = Tenant.find(id)
tenant.name = name if name.present?
tenant.secrets = secrets if secrets.present?

tenant.save!

puts('OK')
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|
check_multitenancy
id = args[:id]
lrs_endpoint = args[:lrs_endpoint]
kc_token_url = args[:kc_token_url]
kc_client_id = args[:kc_client_id]
kc_client_secret = args[:kc_client_secret]
kc_username = args[:kc_username]
kc_password = args[:kc_password]

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')
exit(1)
end

tenant = Tenant.find(id)
tenant.lrs_endpoint = lrs_endpoint
tenant.kc_token_url = kc_token_url
tenant.kc_client_id = kc_client_id
tenant.kc_client_secret = kc_client_secret
tenant.kc_username = kc_username
tenant.kc_password = kc_password

tenant.save!

puts('OK')
Expand Down
6 changes: 6 additions & 0 deletions spec/factories/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
factory :tenant do
name { Faker::Creature::Animal.name }
secrets { "#{Faker::Crypto.sha256}:#{Faker::Crypto.sha512}" }
lrs_endpoint { nil }
kc_token_url { nil }
kc_client_id { nil }
kc_client_secret { nil }
kc_username { nil }
kc_password { nil }
end
end
43 changes: 43 additions & 0 deletions spec/requests/bigbluebutton_api_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,49 @@
end
end
end

context 'secret-lrs-payload' do
before do
tenant.lrs_endpoint = 'https://test.com'

tenant.save!
end

it 'makes a call to the LrsPayloadService and sets meta_secret-lrs-payload' do
allow_any_instance_of(LrsPayloadService).to receive(:call).and_return('test-token')

create_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567" }
stub_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567",
'meta_tenant-id': tenant.id, 'meta_secret-lrs-payload': 'test-token' }

stub_create = stub_request(:get, encode_bbb_uri("create", server.url, server.secret, stub_params))
.to_return(body: "<response><returncode>SUCCESS</returncode><meetingID>test-meeting-1</meetingID>
<attendeePW>ap</attendeePW><moderatorPW>mp</moderatorPW><messageKey/><message/></response>")

get bigbluebutton_api_create_url, params: create_params

response_xml = Nokogiri.XML(response.body)
expect(stub_create).to have_been_requested
expect(response_xml.at_xpath("/response/returncode").text).to eq("SUCCESS")
end

it 'does not set meta_secret-lrs-payload if the value is nil' do
allow_any_instance_of(LrsPayloadService).to receive(:call).and_return(nil)

create_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567" }
stub_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567", 'meta_tenant-id': tenant.id }

stub_create = stub_request(:get, encode_bbb_uri("create", server.url, server.secret, stub_params))
.to_return(body: "<response><returncode>SUCCESS</returncode><meetingID>test-meeting-1</meetingID>
<attendeePW>ap</attendeePW><moderatorPW>mp</moderatorPW><messageKey/><message/></response>")

get bigbluebutton_api_create_url, params: create_params

response_xml = Nokogiri.XML(response.body)
expect(stub_create).to have_been_requested
expect(response_xml.at_xpath("/response/returncode").text).to eq("SUCCESS")
end
end
end
end

Expand Down
44 changes: 44 additions & 0 deletions spec/services/lrs_payload_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

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'
}

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

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(Rails.logger).to receive(:warn)

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

0 comments on commit 5e36183

Please sign in to comment.