Skip to content

Commit 4d62cda

Browse files
authored
Initial work for custom lrs integration (#1035)
* Initial work for custom lrs integration * Added tests
1 parent 013d5e3 commit 4d62cda

File tree

8 files changed

+216
-1
lines changed

8 files changed

+216
-1
lines changed

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ Metrics/PerceivedComplexity:
135135
Exclude:
136136
- app/models/recording.rb
137137
- app/models/server.rb
138+
- app/models/tenant.rb
138139
- lib/server_sync.rb
139140

140141
# Avoid classes longer than 100 lines of code.
@@ -164,6 +165,7 @@ Metrics/CyclomaticComplexity:
164165
Exclude:
165166
- app/models/recording.rb
166167
- app/models/server.rb
168+
- app/models/tenant.rb
167169
- lib/server_sync.rb
168170

169171
# Checks for method parameter names that contain capital letters, end in numbers, or do not meet a minimal length.

app/controllers/bigbluebutton_api_controller.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ def create
198198

199199
params[:voiceBridge] = meeting.voice_bridge
200200

201+
if @tenant&.lrs_endpoint.present?
202+
lrs_payload = LrsPayloadService.new(tenant: @tenant, secret: server.secret).call
203+
params[:'meta_secret-lrs-payload'] = lrs_payload if lrs_payload.present?
204+
end
205+
201206
logger.debug("Creating meeting #{params[:meetingID]} on BigBlueButton server #{server.id}")
202207
params_hash = params
203208

app/models/tenant.rb

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

6-
define_attribute_methods :id, :name, :secrets
6+
define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password
77

88
# Unique ID for this tenant
99
application_redis_attr :id
@@ -14,6 +14,14 @@ class Tenant < ApplicationRedisRecord
1414
# Shared secrets for making API requests for this tenant (: separated)
1515
application_redis_attr :secrets
1616

17+
# Custom LRS work
18+
application_redis_attr :lrs_endpoint
19+
application_redis_attr :kc_token_url
20+
application_redis_attr :kc_client_id
21+
application_redis_attr :kc_client_secret
22+
application_redis_attr :kc_username
23+
application_redis_attr :kc_password
24+
1725
def save!
1826
with_connection do |redis|
1927
raise RecordNotSaved.new('Cannot update id field', self) if id_changed? && !@new_record
@@ -34,6 +42,12 @@ def save!
3442
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
3543
pipeline.hset(id_key, 'name', name) if name_changed?
3644
pipeline.hset(id_key, 'secrets', secrets) if secrets_changed?
45+
pipeline.hset(id_key, 'lrs_endpoint', lrs_endpoint) if lrs_endpoint_changed?
46+
pipeline.hset(id_key, 'kc_token_url', kc_token_url) if kc_token_url_changed?
47+
pipeline.hset(id_key, 'kc_client_id', kc_client_id) if kc_client_id_changed?
48+
pipeline.hset(id_key, 'kc_client_secret', kc_client_secret) if kc_client_secret_changed?
49+
pipeline.hset(id_key, 'kc_username', kc_username) if kc_username_changed?
50+
pipeline.hset(id_key, 'kc_password', kc_password) if kc_password_changed?
3751
pipeline.sadd?('tenants', id) if id_changed?
3852
end
3953
end

app/services/lrs_payload_service.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
class LrsPayloadService
4+
def initialize(tenant:, secret:)
5+
@tenant = tenant
6+
@secret = secret
7+
end
8+
9+
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+
}
23+
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")
31+
return nil
32+
end
33+
34+
parsed_response = JSON.parse(response.body)
35+
kc_access_token = parsed_response['access_token']
36+
37+
lrs_payload = {
38+
lrs_endpoint: @tenant.lrs_endpoint,
39+
lrs_token: kc_access_token
40+
}
41+
42+
# Generate a random salt
43+
salt = SecureRandom.random_bytes(8)
44+
45+
# Generate a key and initialization vector (IV) using PBKDF2 with SHA-256
46+
key_iv = OpenSSL::PKCS5.pbkdf2_hmac(@secret, salt, 10_000, 48, OpenSSL::Digest.new('SHA256'))
47+
key = key_iv[0, 32] # 32 bytes for the key
48+
iv = key_iv[32, 16] # 16 bytes for the IV
49+
50+
# Encrypt the data using AES-256-CBC
51+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
52+
cipher.encrypt
53+
cipher.key = key
54+
cipher.iv = iv
55+
56+
# Encrypt and Base64 encode the data
57+
Base64.strict_encode64(Random.random_bytes(8) + salt + cipher.update(lrs_payload.to_json) + cipher.final)
58+
rescue StandardError => e
59+
Rails.logger.warn("Error #{e} when trying to compute LRS Payload")
60+
61+
nil
62+
end
63+
end

lib/tasks/tenants.rake

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ task tenants: :environment do |_t, _args|
1414
puts("id: #{tenant.id}")
1515
puts("\tname: #{tenant.name}")
1616
puts("\tsecrets: #{tenant.secrets}")
17+
puts("\tlrs_endpoint: #{tenant.lrs_endpoint}") if tenant.lrs_endpoint.present?
18+
puts("\tkc_token_url: #{tenant.kc_token_url}") if tenant.kc_token_url.present?
19+
puts("\tkc_client_id: #{tenant.kc_client_id}") if tenant.kc_client_id.present?
20+
puts("\tkc_client_secret: #{tenant.kc_client_secret}") if tenant.kc_client_secret.present?
21+
puts("\tkc_username: #{tenant.kc_username}") if tenant.kc_username.present?
22+
puts("\tkc_password: #{tenant.kc_password}") if tenant.kc_password.present?
1723
end
1824
end
1925

@@ -53,6 +59,38 @@ namespace :tenants do
5359
tenant = Tenant.find(id)
5460
tenant.name = name if name.present?
5561
tenant.secrets = secrets if secrets.present?
62+
63+
tenant.save!
64+
65+
puts('OK')
66+
puts("Updated Tenant id: #{tenant.id}")
67+
end
68+
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|
71+
check_multitenancy
72+
id = args[:id]
73+
lrs_endpoint = args[:lrs_endpoint]
74+
kc_token_url = args[:kc_token_url]
75+
kc_client_id = args[:kc_client_id]
76+
kc_client_secret = args[:kc_client_secret]
77+
kc_username = args[:kc_username]
78+
kc_password = args[:kc_password]
79+
80+
if id.blank? || lrs_endpoint.blank? || kc_token_url.blank? || kc_client_id.blank? ||
81+
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')
83+
exit(1)
84+
end
85+
86+
tenant = Tenant.find(id)
87+
tenant.lrs_endpoint = lrs_endpoint
88+
tenant.kc_token_url = kc_token_url
89+
tenant.kc_client_id = kc_client_id
90+
tenant.kc_client_secret = kc_client_secret
91+
tenant.kc_username = kc_username
92+
tenant.kc_password = kc_password
93+
5694
tenant.save!
5795

5896
puts('OK')

spec/factories/tenant.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
factory :tenant do
55
name { Faker::Creature::Animal.name }
66
secrets { "#{Faker::Crypto.sha256}:#{Faker::Crypto.sha512}" }
7+
lrs_endpoint { nil }
8+
kc_token_url { nil }
9+
kc_client_id { nil }
10+
kc_client_secret { nil }
11+
kc_username { nil }
12+
kc_password { nil }
713
end
814
end

spec/requests/bigbluebutton_api_controller_spec.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,49 @@
922922
end
923923
end
924924
end
925+
926+
context 'secret-lrs-payload' do
927+
before do
928+
tenant.lrs_endpoint = 'https://test.com'
929+
930+
tenant.save!
931+
end
932+
933+
it 'makes a call to the LrsPayloadService and sets meta_secret-lrs-payload' do
934+
allow_any_instance_of(LrsPayloadService).to receive(:call).and_return('test-token')
935+
936+
create_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567" }
937+
stub_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567",
938+
'meta_tenant-id': tenant.id, 'meta_secret-lrs-payload': 'test-token' }
939+
940+
stub_create = stub_request(:get, encode_bbb_uri("create", server.url, server.secret, stub_params))
941+
.to_return(body: "<response><returncode>SUCCESS</returncode><meetingID>test-meeting-1</meetingID>
942+
<attendeePW>ap</attendeePW><moderatorPW>mp</moderatorPW><messageKey/><message/></response>")
943+
944+
get bigbluebutton_api_create_url, params: create_params
945+
946+
response_xml = Nokogiri.XML(response.body)
947+
expect(stub_create).to have_been_requested
948+
expect(response_xml.at_xpath("/response/returncode").text).to eq("SUCCESS")
949+
end
950+
951+
it 'does not set meta_secret-lrs-payload if the value is nil' do
952+
allow_any_instance_of(LrsPayloadService).to receive(:call).and_return(nil)
953+
954+
create_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567" }
955+
stub_params = { meetingID: "test-meeting-1", moderatorPW: "test-password", voiceBridge: "1234567", 'meta_tenant-id': tenant.id }
956+
957+
stub_create = stub_request(:get, encode_bbb_uri("create", server.url, server.secret, stub_params))
958+
.to_return(body: "<response><returncode>SUCCESS</returncode><meetingID>test-meeting-1</meetingID>
959+
<attendeePW>ap</attendeePW><moderatorPW>mp</moderatorPW><messageKey/><message/></response>")
960+
961+
get bigbluebutton_api_create_url, params: create_params
962+
963+
response_xml = Nokogiri.XML(response.body)
964+
expect(stub_create).to have_been_requested
965+
expect(response_xml.at_xpath("/response/returncode").text).to eq("SUCCESS")
966+
end
967+
end
925968
end
926969
end
927970

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
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+
17+
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+
}
26+
27+
stub_create = stub_request(:post, tenant.kc_token_url)
28+
.with(body: payload).to_return(body: "kc_access_token")
29+
30+
described_class.new(tenant: tenant, secret: 'server-secret').call
31+
32+
expect(stub_create).to have_been_requested
33+
end
34+
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: {})
38+
39+
expect(Rails.logger).to receive(:warn)
40+
41+
expect(described_class.new(tenant: tenant, secret: 'server-secret').call).to be_nil
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)