From 967130e57cfc4e720aab882fa8b748fafa398dea Mon Sep 17 00:00:00 2001 From: Ahmad Farhat Date: Thu, 23 Jan 2020 09:04:41 -0500 Subject: [PATCH] GRN2-253: Added the ability to share rooms across multiple users (#912) * Added ability to share rooms with other users * Fixed testcases --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/room.js | 104 +++++++++++++++ app/assets/stylesheets/application.scss | 4 +- app/assets/stylesheets/rooms.scss | 17 +++ app/assets/stylesheets/users.scss | 8 ++ app/controllers/admins_controller.rb | 17 +-- app/controllers/application_controller.rb | 6 + app/controllers/concerns/populator.rb | 54 ++++++++ app/controllers/concerns/rolify.rb | 2 +- app/controllers/rooms_controller.rb | 76 ++++++++++- app/helpers/admins_helper.rb | 8 ++ app/models/role.rb | 10 +- app/models/room.rb | 10 ++ app/models/setting.rb | 2 + app/models/shared_access.rb | 5 + app/models/user.rb | 17 +++ app/views/admins/components/_roles.html.erb | 5 + .../components/_server_room_row.html.erb | 5 + .../admins/components/_settings.html.erb | 23 +++- app/views/admins/server_rooms.html.erb | 3 + .../rooms/components/_room_block.html.erb | 29 +++-- .../components/_shared_room_block.html.erb | 48 +++++++ app/views/rooms/show.html.erb | 18 ++- app/views/shared/_sessions.html.erb | 2 +- .../modals/_remove_access_modal.html.erb | 39 ++++++ .../shared/modals/_share_room_modal.html.erb | 45 +++++++ config/application.rb | 3 + config/locales/en.yml | 21 ++++ config/routes.rb | 3 + .../20191128212935_create_shared_accesses.rb | 12 ++ db/schema.rb | 11 +- lib/assets/_primary_themes.scss | 3 +- spec/controllers/admins_controller_spec.rb | 60 +++++---- spec/controllers/rooms_controller_spec.rb | 118 ++++++++++++++++++ .../javascripts/bootstrap-select.min.js | 8 ++ .../stylesheets/bootstrap-select.min.css | 6 + 36 files changed, 748 insertions(+), 55 deletions(-) create mode 100644 app/controllers/concerns/populator.rb create mode 100644 app/models/shared_access.rb create mode 100644 app/views/rooms/components/_shared_room_block.html.erb create mode 100644 app/views/shared/modals/_remove_access_modal.html.erb create mode 100644 app/views/shared/modals/_share_room_modal.html.erb create mode 100644 db/migrate/20191128212935_create_shared_accesses.rb create mode 100644 vendor/assets/javascripts/bootstrap-select.min.js create mode 100644 vendor/assets/stylesheets/bootstrap-select.min.css diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index fdab7fc0..6c82146b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -34,4 +34,5 @@ //= require jquery-ui/widget //= require jquery-ui/widgets/sortable //= require pickr.min.js +//= require bootstrap-select.min.js //= require_tree . diff --git a/app/assets/javascripts/room.js b/app/assets/javascripts/room.js index 88ec4cee..1a80be95 100644 --- a/app/assets/javascripts/room.js +++ b/app/assets/javascripts/room.js @@ -60,6 +60,69 @@ $(document).on('turbolinks:load', function(){ $(".delete-room").click(function() { showDeleteRoom(this) }) + + $('.selectpicker').selectpicker({ + liveSearchPlaceholder: "Start searching..." + }); + // Fixes turbolinks issue with bootstrap select + $(window).trigger('load.bs.select.data-api'); + + $(".share-room").click(function() { + // Update the path of save button + $("#save-access").attr("data-path", $(this).data("path")) + + // Get list of users shared with and display them + displaySharedUsers($(this).data("users-path")) + }) + + $("#shareRoomModal").on("show.bs.modal", function() { + $(".selectpicker").selectpicker('val','') + }) + + $(".bootstrap-select").on("click", function() { + $(".bs-searchbox").siblings().hide() + }) + + $(".bs-searchbox input").on("input", function() { + if ($(".bs-searchbox input").val() == '' || $(".bs-searchbox input").val().length < 3) { + $(".bs-searchbox").siblings().hide() + } else { + $(".bs-searchbox").siblings().show() + } + }) + + $(".remove-share-room").click(function() { + $("#remove-shared-confirm").parent().attr("action", $(this).data("path")) + }) + + // User selects an option from the Room Access dropdown + $(".bootstrap-select").on("changed.bs.select", function(){ + // Get the uid of the selected user + let uid = $(".selectpicker").selectpicker('val') + + // If the value was changed to blank, ignore it + if (uid == "") return + + let currentListItems = $("#user-list li").toArray().map(user => $(user).data("uid")) + + // Check to make sure that the user is not already there + if (!currentListItems.includes(uid)) { + // Create the faded list item and display it + let option = $("option[value='" + uid + "']") + + let listItem = document.createElement("li") + listItem.setAttribute('class', 'list-group-item text-left not-saved add-access'); + listItem.setAttribute("data-uid", uid) + + let spanItem = "" + option.text().charAt(0) + " " + + option.text() + " " + option.data("subtext") + "" + + "" + + listItem.innerHTML = spanItem + + $("#user-list").append(listItem) + } + }) } }); @@ -150,3 +213,44 @@ function ResetAccessCode(){ $("#create-room-access-code").text(getLocalizedString("modal.create_room.access_code_placeholder")) $("#room_access_code").val(null) } + +function saveAccessChanges() { + let listItemsToAdd = $("#user-list li:not(.remove-shared)").toArray().map(user => $(user).data("uid")) + + $.post($("#save-access").data("path"), {add: listItemsToAdd}) +} + +// Get list of users shared with and display them +function displaySharedUsers(path) { + $.get(path, function(users) { + // Create list element and add to user list + var user_list_html = "" + + users.forEach(function(user) { + user_list_html += "
  • " + + if (user.image) { + user_list_html += "" + } else { + user_list_html += "" + user.name.charAt(0) + "" + } + user_list_html += "" + user.name + "" + user.uid + "" + user_list_html += "" + user_list_html += "
  • " + }) + + $("#user-list").html(user_list_html) + }); +} + +// Removes the user from the list of shared users +function removeSharedUser(target) { + let parentLI = target.closest("li") + + if (parentLI.classList.contains("not-saved")) { + parentLI.parentNode.removeChild(parentLI) + } else { + parentLI.removeChild(target) + parentLI.classList.add("remove-shared") + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index adde8988..a132061b 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -36,14 +36,16 @@ @import "tabler-custom"; @import "font-awesome-sprockets"; @import "font-awesome"; +@import "monolith.min.scss"; +@import "bootstrap-select.min.css"; @import "utilities/variables"; @import "admins"; @import "main"; @import "rooms"; @import "sessions"; -@import "monolith.min.scss"; @import "utilities/fonts"; +@import "users"; * { outline: none !important; diff --git a/app/assets/stylesheets/rooms.scss b/app/assets/stylesheets/rooms.scss index 0be50823..57f7fdf6 100644 --- a/app/assets/stylesheets/rooms.scss +++ b/app/assets/stylesheets/rooms.scss @@ -83,3 +83,20 @@ margin-top: -6rem; font-size: 5rem; } + +.bootstrap-select .dropdown-menu li.active small.text-muted{ + color: #9aa0ac !important +} + +.not-saved { + color: grey; + background: rgba(0, 40, 100, 0.12); +} + +.dropdown-menu.show { + min-height: 0px !important; +} + +.remove-shared { + text-decoration: line-through; +} \ No newline at end of file diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index a0ec9aee..8d44447a 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -21,4 +21,12 @@ .user-role-tag{ color: white !important; +} + +.shared-user { + line-height: 30px; +} + +.bootstrap-select { + border: 1px solid rgba(0, 40, 100, 0.12); } \ No newline at end of file diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index 84395d70..9a664e43 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -22,6 +22,7 @@ class AdminsController < ApplicationController include Emailer include Recorder include Rolify + include Populator manage_users = [:edit_user, :promote, :demote, :ban_user, :unban_user, :approve, :reset] manage_deleted_users = [:undelete] @@ -49,11 +50,7 @@ class AdminsController < ApplicationController # GET /admins/server_recordings def server_recordings - server_rooms = if Rails.configuration.loadbalanced_configuration - Room.includes(:owner).where(users: { provider: @user_domain }).pluck(:bbb_id) - else - Room.pluck(:bbb_id) - end + server_rooms = rooms_list_for_recordings @search, @order_column, @order_direction, recs = all_recordings(server_rooms, params.permit(:search, :column, :direction), true, true) @@ -67,13 +64,9 @@ class AdminsController < ApplicationController @order_column = params[:column] && params[:direction] != "none" ? params[:column] : "created_at" @order_direction = params[:direction] && params[:direction] != "none" ? params[:direction] : "DESC" - server_rooms = if Rails.configuration.loadbalanced_configuration - Room.includes(:owner).where(users: { provider: @user_domain }) - .admins_search(@search) - .admins_order(@order_column, @order_direction) - else - Room.all.admins_search(@search).admins_order(@order_column, @order_direction) - end + server_rooms = server_rooms_list + + @user_list = shared_user_list if shared_access_allowed @pagy, @rooms = pagy_array(server_rooms) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 96cd481f..b220d2a3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -172,6 +172,12 @@ class ApplicationController < ActionController::Base end helper_method :configured_providers + # Indicates whether users are allowed to share rooms + def shared_access_allowed + @settings.get_value("Shared Access") == "true" + end + helper_method :shared_access_allowed + # Parses the url for the user domain def parse_user_domain(hostname) return hostname.split('.').first if Rails.configuration.url_host.empty? diff --git a/app/controllers/concerns/populator.rb b/app/controllers/concerns/populator.rb new file mode 100644 index 00000000..41c999dc --- /dev/null +++ b/app/controllers/concerns/populator.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with BigBlueButton; if not, see . + +module Populator + extend ActiveSupport::Concern + + # Returns a list of rooms that are in the same context of the current user + def server_rooms_list + if Rails.configuration.loadbalanced_configuration + Room.includes(:owner).where(users: { provider: @user_domain }) + .admins_search(@search) + .admins_order(@order_column, @order_direction) + else + Room.all.admins_search(@search).admins_order(@order_column, @order_direction) + end + end + + # Returns list of rooms needed to get the recordings on the server + def rooms_list_for_recordings + if Rails.configuration.loadbalanced_configuration + Room.includes(:owner).where(users: { provider: @user_domain }).pluck(:bbb_id) + else + Room.pluck(:bbb_id) + end + end + + # Returns a list of users that are in the same context of the current user + def shared_user_list + roles_can_appear = [] + Role.where(provider: @user_domain).each do |role| + roles_can_appear << role.name if role.get_permission("can_appear_in_share_list") && role.name != "super_admin" + end + + initial_list = User.where.not(uid: current_user.uid).with_highest_priority_role(roles_can_appear) + + return initial_list unless Rails.configuration.loadbalanced_configuration + initial_list.where(provider: @user_domain) + end +end diff --git a/app/controllers/concerns/rolify.rb b/app/controllers/concerns/rolify.rb index c7bf63fd..c22691de 100644 --- a/app/controllers/concerns/rolify.rb +++ b/app/controllers/concerns/rolify.rb @@ -142,7 +142,7 @@ module Rolify role_params = params.require(:role).permit(:name) permission_params = params.require(:role).permit(:can_create_rooms, :send_promoted_email, :send_demoted_email, :can_edit_site_settings, :can_edit_roles, :can_manage_users, - :can_manage_rooms_recordings, :colour) + :can_manage_rooms_recordings, :can_appear_in_share_list, :colour) permission_params.transform_values! do |v| if v == "0" diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb index 02c4ac92..001bc8b1 100644 --- a/app/controllers/rooms_controller.rb +++ b/app/controllers/rooms_controller.rb @@ -20,12 +20,15 @@ class RoomsController < ApplicationController include Pagy::Backend include Recorder include Joiner + include Populator before_action :validate_accepted_terms, unless: -> { !Rails.configuration.terms } before_action :validate_verified_email, except: [:show, :join], unless: -> { !Rails.configuration.enable_email_verification } before_action :find_room, except: [:create, :join_specific_room] - before_action :verify_room_ownership_or_admin, only: [:start, :update_settings, :destroy] + before_action :verify_room_ownership_or_admin_or_shared, only: [:start, :shared_access] + before_action :verify_room_ownership_or_admin, only: [:update_settings, :destroy] + before_action :verify_room_ownership_or_shared, only: [:remove_shared_access] before_action :verify_room_owner_verified, only: [:show, :join], unless: -> { !Rails.configuration.enable_email_verification } before_action :verify_room_owner_valid, only: [:show, :join] @@ -61,14 +64,17 @@ class RoomsController < ApplicationController def show @anyone_can_start = JSON.parse(@room[:room_settings])["anyoneCanStart"] @room_running = room_running?(@room.bbb_id) + @shared_room = room_shared_with_user # If its the current user's room - if current_user && @room.owned_by?(current_user) + if current_user && (@room.owned_by?(current_user) || @shared_room) if current_user.highest_priority_role.get_permission("can_create_rooms") # User is allowed to have rooms @search, @order_column, @order_direction, recs = recordings(@room.bbb_id, params.permit(:search, :column, :direction), true) + @user_list = shared_user_list if shared_access_allowed + @pagy, @recordings = pagy_array(recs) else # Render view for users that cant create rooms @@ -189,6 +195,55 @@ class RoomsController < ApplicationController redirect_back fallback_location: room_path(@room) end + # POST /:room_uid/update_shared_access + def shared_access + begin + current_list = @room.shared_users.pluck(:id) + new_list = User.where(uid: params[:add]).pluck(:id) + + # Get the list of users that used to be in the list but were removed + users_to_remove = current_list - new_list + # Get the list of users that are in the new list but not in the current list + users_to_add = new_list - current_list + + # Remove users that are removed + SharedAccess.where(room_id: @room.id, user_id: users_to_remove).delete_all unless users_to_remove.empty? + + # Add users that are added + users_to_add.each do |id| + SharedAccess.create(room_id: @room.id, user_id: id) + end + + flash[:success] = I18n.t("room.shared_access_success") + rescue => e + logger.error "Support: Error in updating room shared access: #{e}" + flash[:alert] = I18n.t("room.shared_access_error") + end + + redirect_back fallback_location: room_path + end + + # POST /:room_uid/remove_shared_access + def remove_shared_access + begin + SharedAccess.find_by!(room_id: @room.id, user_id: params[:user_id]).destroy + flash[:success] = I18n.t("room.remove_shared_access_success") + rescue => e + logger.error "Support: Error in removing room shared access: #{e}" + flash[:alert] = I18n.t("room.remove_shared_access_error") + end + + redirect_to current_user.main_room + end + + # GET /:room_uid/shared_users + def shared_users + # Respond with JSON object of users that have access to the room + respond_to do |format| + format.json { render body: @room.shared_users.to_json } + end + end + # GET /:room_uid/logout def logout logger.info "Support: #{current_user.present? ? current_user.email : 'Guest'} has left room #{@room.uid}" @@ -229,11 +284,23 @@ class RoomsController < ApplicationController @room = Room.find_by!(uid: params[:room_uid]) end + # Ensure the user either owns the room or is an admin of the room owner or the room is shared with him + def verify_room_ownership_or_admin_or_shared + return redirect_to root_path unless @room.owned_by?(current_user) || + room_shared_with_user || + current_user&.admin_of?(@room.owner) + end + # Ensure the user either owns the room or is an admin of the room owner def verify_room_ownership_or_admin return redirect_to root_path if !@room.owned_by?(current_user) && !current_user&.admin_of?(@room.owner) end + # Ensure the user owns the room or is allowed to start it + def verify_room_ownership_or_shared + return redirect_to root_path unless @room.owned_by?(current_user) || room_shared_with_user + end + def validate_accepted_terms redirect_to terms_path if current_user && !current_user&.accepted_terms end @@ -259,6 +326,11 @@ class RoomsController < ApplicationController @settings.get_value("Room Authentication") == "true" && current_user.nil? end + # Checks if the room is shared with the user and room sharing is enabled + def room_shared_with_user + shared_access_allowed ? @room.shared_with?(current_user) : false + end + def room_limit_exceeded limit = @settings.get_value("Room Limit").to_i diff --git a/app/helpers/admins_helper.rb b/app/helpers/admins_helper.rb index aff41f2a..381938c2 100644 --- a/app/helpers/admins_helper.rb +++ b/app/helpers/admins_helper.rb @@ -37,6 +37,14 @@ module AdminsHelper end end + def shared_access_string + if @settings.get_value("Shared Access") == "true" + I18n.t("administrator.site_settings.authentication.enabled") + else + I18n.t("administrator.site_settings.authentication.disabled") + end + end + def recording_default_visibility_string if @settings.get_value("Default Recording Visibility") == "public" I18n.t("recording.visibility.public") diff --git a/app/models/role.rb b/app/models/role.rb index eb2ff585..6ea43d36 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -42,7 +42,7 @@ class Role < ApplicationRecord Role.create(name: "super_admin", provider: provider, priority: -2, colour: "#cd201f") .update_all_role_permissions(can_create_rooms: true, send_promoted_email: true, send_demoted_email: true, can_edit_site_settings: true, - can_edit_roles: true, can_manage_users: true) + can_edit_roles: true, can_manage_users: true, can_appear_in_share_list: true) end def self.create_new_role(role_name, provider) @@ -69,6 +69,7 @@ class Role < ApplicationRecord update_permission("can_edit_roles", permissions[:can_edit_roles].to_s) update_permission("can_manage_users", permissions[:can_manage_users].to_s) update_permission("can_manage_rooms_recordings", permissions[:can_manage_rooms_recordings].to_s) + update_permission("can_appear_in_share_list", permissions[:can_appear_in_share_list].to_s) end # Updates the value of the permission and enables it @@ -85,7 +86,12 @@ class Role < ApplicationRecord value = if permission[:enabled] permission[:value] else - "false" + case name + when "can_appear_in_share_list" + Rails.configuration.shared_access_default.to_s + else + "false" + end end if return_boolean diff --git a/app/models/room.rb b/app/models/room.rb index 7c7e7231..a5bc17a3 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -26,6 +26,7 @@ class Room < ApplicationRecord validates :name, presence: true belongs_to :owner, class_name: 'User', foreign_key: :user_id + has_many :shared_access def self.admins_search(string) active_database = Rails.configuration.database_configuration[Rails.env]["adapter"] @@ -59,6 +60,15 @@ class Room < ApplicationRecord user.rooms.include?(self) end + def shared_users + User.where(id: shared_access.pluck(:user_id)) + end + + def shared_with?(user) + return false if user.nil? + shared_users.include?(user) + end + # Determines the invite path for the room. def invite_path "#{Rails.configuration.relative_url_root}/#{CGI.escape(uid)}" diff --git a/app/models/setting.rb b/app/models/setting.rb index 82f395f5..cf2254a6 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -43,6 +43,8 @@ class Setting < ApplicationRecord false when "Room Limit" Rails.configuration.number_of_rooms_default + when "Shared Access" + Rails.configuration.shared_access_default end end end diff --git a/app/models/shared_access.rb b/app/models/shared_access.rb new file mode 100644 index 00000000..eb88a555 --- /dev/null +++ b/app/models/shared_access.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class SharedAccess < ApplicationRecord + belongs_to :room +end diff --git a/app/models/user.rb b/app/models/user.rb index 4cae3de9..a4214f09 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -29,6 +29,7 @@ class User < ApplicationRecord before_destroy :destroy_rooms has_many :rooms + has_many :shared_access belongs_to :main_room, class_name: 'Room', foreign_key: :room_id, required: false has_and_belongs_to_many :roles, -> { includes :role_permissions }, join_table: :users_roles @@ -135,6 +136,11 @@ class User < ApplicationRecord room_list.where.not(last_session: nil).order("last_session desc") + room_list.where(last_session: nil) end + # Retrieves a list of rooms that are shared with the user + def shared_rooms + Room.where(id: shared_access.pluck(:room_id)) + end + def name_chunk charset = ("a".."z").to_a - %w(b i l o s) + ("2".."9").to_a - %w(5 8) chunk = name.parameterize[0...3] @@ -228,11 +234,22 @@ class User < ApplicationRecord User.where.not(id: with_role(role).pluck(:id)) end + def self.with_highest_priority_role(role) + User.all_users_highest_priority_role.where(roles: { name: role }) + end + def self.all_users_with_roles User.joins("INNER JOIN users_roles ON users_roles.user_id = users.id INNER JOIN roles " \ "ON roles.id = users_roles.role_id INNER JOIN role_permissions ON roles.id = role_permissions.role_id").distinct end + def self.all_users_highest_priority_role + User.joins("INNER JOIN (SELECT user_id, role_id, min(roles.priority) FROM users_roles " \ + "INNER JOIN roles ON users_roles.role_id = roles.id GROUP BY user_id) as a ON " \ + "a.user_id = users.id INNER JOIN roles ON roles.id = a.role_id " \ + " INNER JOIN role_permissions ON roles.id = role_permissions.role_id").distinct + end + private def create_reset_activation_digest(token) diff --git a/app/views/admins/components/_roles.html.erb b/app/views/admins/components/_roles.html.erb index ee4d0edf..77a316e1 100644 --- a/app/views/admins/components/_roles.html.erb +++ b/app/views/admins/components/_roles.html.erb @@ -73,6 +73,11 @@ <%= f.check_box :can_edit_roles, checked: @selected_role.get_permission("can_edit_roles"), class: "custom-switch-input", disabled: edit_disabled || !current_role.get_permission("can_edit_roles") %> +