diff --git a/app/controllers/api/v1/recordings_controller.rb b/app/controllers/api/v1/recordings_controller.rb index 7255133ce4e992d8266cacd70465f8ab1077ef59..950f232e818684c790a604b805547d77de2b145c 100644 --- a/app/controllers/api/v1/recordings_controller.rb +++ b/app/controllers/api/v1/recordings_controller.rb @@ -19,12 +19,14 @@ module Api module V1 class RecordingsController < ApiController + skip_before_action :ensure_authenticated, only: :recording_url + before_action :find_recording, only: %i[update update_visibility recording_url] before_action only: %i[destroy] do ensure_authorized('ManageRecordings', record_id: params[:id]) end before_action only: %i[update update_visibility recording_url] do - ensure_authorized(%w[ManageRecordings SharedRoom], record_id: params[:id]) + ensure_authorized(%w[ManageRecordings SharedRoom PublicRecordings], record_id: params[:id]) end before_action only: %i[index recordings_count] do ensure_authorized('CreateRoom') @@ -92,16 +94,29 @@ module Api def recording_url record_format = params[:recording_format] - url = if @recording.visibility == 'Protected' - recording = BigBlueButtonApi.new(provider: current_provider).get_recording(record_id: @recording.record_id) - formats = recording[:playback][:format] - - record_format.present? ? formats.find { |format| format[:type] == record_format }[:url] : formats.pluck(:url) - else - record_format.present? ? @recording.formats.find_by(recording_type: record_format).url : @recording.formats.pluck(:url) - end - - render_data data: url, status: :ok + urls = if [Recording::VISIBILITIES[:protected], Recording::VISIBILITIES[:public_protected]].include? @recording.visibility + recording = BigBlueButtonApi.new(provider: current_provider).get_recording(record_id: @recording.record_id) + formats = recording[:playback][:format] + formats = [formats] unless formats.is_a? Array + + if record_format.present? + found_format = formats.find { |format| format[:type] == record_format } + return render_error status: :not_found unless found_format + + found_format[:url] + else + formats.pluck(:url) + end + elsif record_format.present? + found_format = @recording.formats.find_by(recording_type: record_format) + return render_error status: :not_found unless found_format + + found_format[:url] + else + @recording.formats.pluck(:url) + end + + render_data data: urls, status: :ok end private diff --git a/app/services/permissions_checker.rb b/app/services/permissions_checker.rb index 39f878330523fcedecd65c76d3864e9a4c68b4c3..380251c6cd246a42d046703c46aeb0038fdc7e34 100644 --- a/app/services/permissions_checker.rb +++ b/app/services/permissions_checker.rb @@ -16,6 +16,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity class PermissionsChecker def initialize(current_user:, permission_names:, current_provider:, user_id: nil, friendly_id: nil, record_id: nil) @current_user = current_user @@ -26,20 +27,29 @@ class PermissionsChecker @current_provider = current_provider end + # The permission checker checks if: + # 1. A current_user exists in the context: + # 1.1. Checks if the current_user is a SuperAdmin and allow it without further checks. + # 1.2. Ensures that the current_user provider and the target resource provider matches. + # 1.3. Ensures that the the current_user is active. + # 1.4. Checks if the current_user is allowed to access a whole class of resources AKA admin (has at least one permission enabled for its role). + # 2. Checks if the current_user is not an admin but allowed to access that specific target resource. def call - # check to see if current_user has SuperAdmin role - return true if @current_user.role == Role.find_by(name: 'SuperAdmin', provider: 'bn') - - # checking if user is trying to access users/rooms/recordings from different provider - return false unless current_provider_check - # Make sure the user is not banned or pending - return false unless @current_user.active? - - return true if RolePermission.joins(:permission).exists?( - role_id: @current_user.role_id, - permission: { name: @permission_names }, - value: 'true' - ) + if @current_user + # check to see if current_user has SuperAdmin role + return true if @current_user.role == Role.find_by(name: 'SuperAdmin', provider: 'bn') + + # checking if user is trying to access users/rooms/recordings from different provider + return false unless current_provider_check + # Make sure the user is not banned or pending + return false unless @current_user.active? + + return true if RolePermission.joins(:permission).exists?( + role_id: @current_user.role_id, + permission: { name: @permission_names }, + value: 'true' + ) + end # convert to array if only checking for 1 permission (for simplicity) Array(@permission_names).each do |permission| @@ -54,6 +64,8 @@ class PermissionsChecker return true if authorize_manage_recordings when 'RoomLimit' return true if authorize_room_limit + when 'PublicRecordings' + return true if authorize_public_recordings end end @@ -62,31 +74,53 @@ class PermissionsChecker private + # Ensures that the current_user is requesting access to manage its account specifically. def authorize_manage_user + return false if @current_user.blank? return false if @user_id.blank? @current_user.id.to_s == @user_id.to_s end + # Ensures that the current_user is requesting access to manage one of its rooms specifically. def authorize_manage_rooms + return false if @current_user.blank? return false if @friendly_id.blank? @current_user.rooms.find_by(friendly_id: @friendly_id).present? end + # Ensures that the current_user is requesting access to manage a shared room specifically. def authorize_shared_room + return false if @current_user.blank? return false if @friendly_id.blank? && @record_id.blank? @current_user.shared_rooms.exists?(friendly_id: @friendly_id) || @current_user.shared_rooms.exists?(id: Recording.find_by(record_id: @record_id)&.room_id) end + # Ensures that the current_user is requesting access to manage its rooms recordings specifically. def authorize_manage_recordings + return false if @current_user.blank? return false if @record_id.blank? @current_user.recordings.find_by(record_id: @record_id).present? end + # Ensures that the request is to access and manage a publicly accessible recording. + def authorize_public_recordings + return false if @record_id.blank? + return false if @current_provider.blank? + + recording = Recording.find_by(record_id: @record_id) + return false unless recording + return false if recording.user.provider != @current_provider + return false unless [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]].include? recording.visibility + + true + end + + # Checks if the target user has reached its role room limit. def authorize_room_limit return false if @user_id.blank? @@ -99,7 +133,10 @@ class PermissionsChecker true end + # Checks if the current_user is requsting to access a resource of the same provider. def current_provider_check + return false if @current_user.blank? || @current_user.provider.blank? || @current_provider.blank? + # check to see if current user is trying to access another provider return false if @current_user.provider != @current_provider @@ -115,3 +152,4 @@ class PermissionsChecker true end end +# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity diff --git a/spec/controllers/recordings_controller_spec.rb b/spec/controllers/recordings_controller_spec.rb index a3f4c3830fbf0f8fb6727f6543e6cfca4bc55256..fd8a53c35871cb586ba413d96954ca0a639d5e5a 100644 --- a/spec/controllers/recordings_controller_spec.rb +++ b/spec/controllers/recordings_controller_spec.rb @@ -301,40 +301,202 @@ RSpec.describe Api::V1::RecordingsController, type: :controller do end context 'format not passed' do - it 'makes a call to BBB and returns the url returned if the recording is protected' do - recording = create(:recording, visibility: 'Protected', room:) + context 'Protected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:protected], + Recording::VISIBILITIES[:public_protected] + ].sample, + room:) + end + + it 'makes a call to BBB and returns the URLs' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to match_array ['https://test.com/screenshare', 'https://test.com/video'] + end + + context 'Single playback format' do + before do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_recording).and_return( + playback: { format: { type: 'screenshare', url: 'https://test.com/screenshare' } } + ) + end + + it 'makes a call to BBB and returns the URL' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq ['https://test.com/screenshare'] + end + end + end - post :recording_url, params: { id: recording.record_id } + context 'Unprotected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:published], + Recording::VISIBILITIES[:unpublished], + Recording::VISIBILITIES[:public] + ].sample, + room:) + end + + before { create_list(:format, Faker::Number.within(range: 1..3), recording:) } - expect(JSON.parse(response.body)).to match_array ['https://test.com/screenshare', 'https://test.com/video'] + it 'returns the recording record formats URLs' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to match_array recording.formats.pluck(:url) + end end + end + + context 'format is passed' do + context 'Protected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:protected], + Recording::VISIBILITIES[:public_protected] + ].sample, + room:) + end + + before { create(:format, recording:, recording_type: 'screenshare', url: 'https://invalid.com/screenshare') } + + it 'makes a call to BBB and returns the URL' do + post :recording_url, params: { id: recording.record_id, recording_format: 'screenshare' } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to eq 'https://test.com/screenshare' + end + + context 'Single playback format' do + before do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_recording).and_return( + playback: { format: { type: 'screenshare', url: 'https://test.com/screenshare/solo' } } + ) + end + + it 'makes a call to BBB and returns the URL' do + post :recording_url, params: { id: recording.record_id, recording_format: 'screenshare' } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to eq 'https://test.com/screenshare/solo' + end + end + + context 'Inexistent format' do + it 'returns :not_found' do + post :recording_url, params: { id: recording.record_id, recording_format: '404' } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['data']).to be_blank + end + end + end + + context 'Unprotected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:published], + Recording::VISIBILITIES[:unpublished], + Recording::VISIBILITIES[:public] + ].sample, + room:) + end + + let(:target_format) { create(:format, recording:, recording_type: 'screenshare') } + + before { create_list(:format, 2, recording:) } + + it 'returns the formats URL' do + post :recording_url, params: { id: recording.record_id, recording_format: target_format.recording_type } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to eq target_format.url + end + + context 'Inexistent format' do + it 'returns :not_found' do + post :recording_url, params: { id: recording.record_id, recording_format: '404' } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['data']).to be_blank + end + end + end + end - it 'returns the formats url' do - recording = create(:recording, visibility: 'Published', room:) - create(:format, recording:) + context 'Other users' do + let(:recording) { create(:recording, room:) } + let(:signed_in_user) { create(:user) } + + before { sign_in_user(signed_in_user) } + it 'returns :forbidden' do post :recording_url, params: { id: recording.record_id } - expect(JSON.parse(response.body)).to match_array recording.formats.pluck(:url) + expect(response).to have_http_status(:forbidden) + end + + context 'shared room' do + before do + create(:shared_access, user_id: signed_in_user.id, room_id: room.id) + end + + it 'returns :ok' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + end + end + + context 'Recordings Manager' do + before { sign_in_user(user_with_manage_recordings_permission) } + + it 'returns :ok' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + end end end - context 'format is passed' do - it 'makes a call to BBB and returns the url returned if the recording is protected' do - recording = create(:recording, visibility: 'Protected', room:) + context 'Unauthenticated' do + before { sign_out_user } + + describe 'Public recordings' do + let(:recording) { create(:recording, visibility: [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]].sample) } - post :recording_url, params: { id: recording.record_id, recording_format: 'screenshare' } + it 'returns :ok' do + post :recording_url, params: { id: recording.record_id } - expect(JSON.parse(response.body)['data']).to eq 'https://test.com/screenshare' + expect(response).to have_http_status(:ok) + end end - it 'returns the formats url' do - recording = create(:recording, visibility: 'Published', room:) - format = create(:format, recording:, recording_type: 'podcast') + describe 'Private recordings' do + let(:recording) do + create(:recording, + visibility: [Recording::VISIBILITIES[:protected], Recording::VISIBILITIES[:published], Recording::VISIBILITIES[:unpublished]].sample) + end + + it 'returns :forbidden' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:forbidden) + end + end + end - post :recording_url, params: { id: recording.record_id, recording_format: format.recording_type } + context 'Inexistent recording' do + it 'returns :not_found' do + post :recording_url, params: { id: '404' } - expect(JSON.parse(response.body)['data']).to eq format.url + expect(response).to have_http_status(:not_found) end end end diff --git a/spec/services/permissions_checker_spec.rb b/spec/services/permissions_checker_spec.rb index 339f61ebb3a9e52af6b1d9c16ec2bdd29d85cb2e..99a0d079393d57edce6f1070e8a95bb480762d5a 100644 --- a/spec/services/permissions_checker_spec.rb +++ b/spec/services/permissions_checker_spec.rb @@ -170,5 +170,143 @@ describe PermissionsChecker, type: :service do end end end + + context 'Public recordings' do + let(:public_recording) { create(:recording, visibility: [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]].sample) } + + let(:private_recording) do + create(:recording, + visibility: [Recording::VISIBILITIES[:published], Recording::VISIBILITIES[:protected], Recording::VISIBILITIES[:unpublished]].sample) + end + + it 'returns true if the recording is public' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: public_recording.record_id, + current_provider: public_recording.user.provider + ).call).to be(true) + end + + it 'returns false if the recording is private' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: private_recording.record_id, + current_provider: private_recording.user.provider + ).call).to be(false) + end + + describe 'Differnt provider' do + it 'returns false' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: public_recording.record_id, + current_provider: "NOT_#{public_recording.user.provider}" + ).call).to be(false) + end + end + + describe 'Inexistent recording' do + it 'returns false' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: '404', + current_provider: public_recording.user.provider + ).call).to be(false) + end + end + end + + context 'ManageRecordings' do + let(:current_user) { create(:user) } + + describe 'Recording owned by current user' do + let(:room) { create(:room, user: current_user) } + let(:recording) { create(:recording, room:) } + + it 'returns true' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: current_user.provider + ).call).to be(true) + end + end + + describe 'Recording not owned by current user' do + let(:recording) { create(:recording) } + + it 'returns false' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: current_user.provider + ).call).to be(false) + end + + context 'User with ManageRecordings permission' do + let(:current_user) { create(:user, :with_manage_recordings_permission) } + + describe 'Same provider' do + it 'returns true' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: current_user.provider + ).call).to be(true) + end + end + + describe 'Different provider' do + it 'returns true' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: "NOT_#{recording.user.provider}" + ).call).to be(false) + end + end + end + end + + describe 'Unauthenticated user' do + let(:current_user) { nil } + let(:recording) { create(:recording) } + + it 'returns false' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: recording.room.user.provider + ).call).to be(false) + end + end + end end end