From 4fc1714db8732955f9144e3fab0e8a8af916d32e Mon Sep 17 00:00:00 2001 From: shawn-higgins1 <23224097+shawn-higgins1@users.noreply.github.com> Date: Wed, 31 Jul 2019 11:53:32 -0400 Subject: [PATCH] GRN2-176: Create a role editor that allows admins to specify what permissions each role has (#709) * Add roles editor * Add colour selection ability to roles * Add ability to assign roles to users in the UI * Remove rolify and replace it with our own custom roles implemenation * - Fix all existing roles functionality - Fix super admins * Fix bugs with new customers not have default roles * Add can't create room setting * Code improvements * Fix migration * Add tests for new methods * Translate reserved role names * Pull roles from saml/ldap * Fix rspec * Fix scrutinizer issues * Fix email promoted/demoted tests * Apply comments * Redirect directly to the main room * Add comments --- .gitignore | 3 + Gemfile | 3 +- Gemfile.lock | 5 +- app/assets/javascripts/admins.js | 126 +++++--- app/assets/javascripts/application.js | 2 + app/assets/javascripts/room.js.erb | 5 + app/assets/javascripts/user_edit.js | 88 ++++++ app/assets/stylesheets/admins.scss | 25 ++ app/assets/stylesheets/application.scss | 1 + app/assets/stylesheets/users.scss | 4 + .../utilities/_primary_themes.scss | 11 + app/controllers/admins_controller.rb | 179 +++++++++-- app/controllers/application_controller.rb | 6 +- app/controllers/concerns/emailer.rb | 10 +- app/controllers/recordings_controller.rb | 4 +- app/controllers/rooms_controller.rb | 34 ++- app/controllers/users_controller.rb | 67 ++++- app/helpers/admins_helper.rb | 4 + app/helpers/application_helper.rb | 20 +- app/helpers/rooms_helper.rb | 4 +- app/helpers/theming_helper.rb | 2 +- app/helpers/users_helper.rb | 16 + app/mailers/user_mailer.rb | 10 +- app/models/ability.rb | 23 +- app/models/role.rb | 26 +- app/models/user.rb | 91 +++++- .../admins/components/_menu_buttons.html.erb | 23 +- app/views/admins/components/_roles.html.erb | 94 ++++++ app/views/admins/components/_users.html.erb | 14 +- app/views/admins/roles.html.erb | 27 ++ app/views/rooms/cant_create_rooms.html.erb | 44 +++ app/views/shared/_header.html.erb | 21 +- .../shared/components/_admins_role.html.erb | 24 +- .../shared/components/_admins_tags.html.erb | 42 +-- .../shared/modals/_create_role_modal.html.erb | 44 +++ app/views/shared/settings/_account.html.erb | 22 ++ app/views/user_mailer/user_demoted.html.erb | 4 +- app/views/user_mailer/user_demoted.text.erb | 4 +- app/views/user_mailer/user_promoted.html.erb | 6 +- app/views/user_mailer/user_promoted.text.erb | 6 +- config/initializers/rolify.rb | 12 - config/locales/en.yml | 50 +++- config/routes.rb | 10 +- db/migrate/20190726153012_add_custom_roles.rb | 110 +++++++ db/schema.rb | 100 ++++--- db/seeds.rb | 1 + spec/controllers/admins_controller_spec.rb | 283 +++++++++++++++--- spec/controllers/rooms_controller_spec.rb | 41 +++ spec/controllers/users_controller_spec.rb | 89 ++++++ spec/helpers/admins_helper_spec.rb | 44 +++ spec/helpers/application_helper_spec.rb | 10 + spec/helpers/users_helper_spec.rb | 44 +++ spec/models/role_spec.rb | 35 +++ spec/models/user_spec.rb | 66 ++++ test/mailers/previews/user_mailer_preview.rb | 8 +- yarn.lock | 4 + 56 files changed, 1718 insertions(+), 333 deletions(-) create mode 100644 app/assets/javascripts/user_edit.js create mode 100644 app/views/admins/components/_roles.html.erb create mode 100644 app/views/admins/roles.html.erb create mode 100644 app/views/rooms/cant_create_rooms.html.erb create mode 100644 app/views/shared/modals/_create_role_modal.html.erb delete mode 100644 config/initializers/rolify.rb create mode 100644 db/migrate/20190726153012_add_custom_roles.rb create mode 100644 spec/helpers/admins_helper_spec.rb create mode 100644 spec/helpers/users_helper_spec.rb create mode 100644 spec/models/role_spec.rb create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 6014a7aa65..a9ff8afb05 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ vendor/bundle .env env +# Ignore yarn configs +/node_modules + # IDEs .idea .idea/** diff --git a/Gemfile b/Gemfile index 69d6650249..7e8331c349 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem 'mini_racer', platforms: :ruby # Use jquery as the JavaScript library gem 'jquery-rails', '~> 4.3.3' +gem 'jquery-ui-rails' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' @@ -72,8 +73,6 @@ gem 'redcarpet' # For health check endpoint gem "health_check" -# For providing user roles -gem "rolify" # For limiting access based on user roles gem 'cancancan', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 89ccfcddd3..5e1901f318 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,6 +149,8 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + jquery-ui-rails (6.0.1) + railties (>= 3.2.16) json (2.2.0) jwt (2.2.1) libv8 (7.3.492.27.1) @@ -257,7 +259,6 @@ GEM syslog_protocol request_store (1.4.1) rack (>= 1.4) - rolify (5.2.0) rspec-core (3.8.2) rspec-support (~> 3.8.0) rspec-expectations (3.8.4) @@ -363,6 +364,7 @@ DEPENDENCIES i18n-language-mapping (~> 0.1.0) jbuilder (~> 2.5) jquery-rails (~> 4.3.3) + jquery-ui-rails listen (~> 3.0.5) lograge mini_racer @@ -381,7 +383,6 @@ DEPENDENCIES recaptcha redcarpet remote_syslog_logger - rolify rspec-rails (~> 3.7) rubocop sassc-rails diff --git a/app/assets/javascripts/admins.js b/app/assets/javascripts/admins.js index 0b27efe0cc..8852156791 100644 --- a/app/assets/javascripts/admins.js +++ b/app/assets/javascripts/admins.js @@ -19,47 +19,60 @@ $(document).on('turbolinks:load', function(){ var action = $("body").data('action'); // Only run on the admins page. - if (controller == "admins" && action == "index") { - // show the modal with the correct form action url - $(".delete-user").click(function(data){ - var uid = $(data.target).closest("tr").data("user-uid") - var url = $("body").data("relative-root") - if (!url.endsWith("/")) { - url += "/" - } - url += "u/" + uid - $("#delete-confirm").parent().attr("action", url) - }) - - //clear the role filter if user clicks on the x - $(".clear-role").click(function() { - var search = new URL(location.href).searchParams.get('search') - - var url = window.location.pathname + "?page=1" - - if (search) { - url += "&search=" + search - } - - window.location.replace(url); - }) - } - - if (controller == "admins" && action == "site_settings") { - loadColourSelectors() - } - - // Only run on the admins edit user page. - if (controller == "admins" && action == "edit_user") { - $(".setting-btn").click(function(data){ - var url = $("body").data("relative-root") - if (!url.endsWith("/")) { - url += "/" - } - url += "admins?setting=" + data.target.id - - window.location.href = url - }) + if (controller == "admins") { + if(action == "index") { + // show the modal with the correct form action url + $(".delete-user").click(function(data){ + var uid = $(data.target).closest("tr").data("user-uid") + var url = $("body").data("relative-root") + if (!url.endsWith("/")) { + url += "/" + } + url += "u/" + uid + $("#delete-confirm").parent().attr("action", url) + }) + + //clear the role filter if user clicks on the x + $(".clear-role").click(function() { + var search = new URL(location.href).searchParams.get('search') + + var url = window.location.pathname + "?page=1" + + if (search) { + url += "&search=" + search + } + + window.location.replace(url); + }) + } + else if(action == "site_settings"){ + loadColourSelectors() + } + else if (action == "roles"){ + // Refreshes the new role modal + $("#newRoleButton").click(function(){ + $("#createRoleName").val("") + }) + + // Updates the colour picker to the correct colour + role_colour = $("#role-colorinput-regular").data("colour") + $("#role-colorinput-regular").css("background-color", role_colour); + $("#role-colorinput-regular").css("border-color", role_colour); + + loadRoleColourSelector(role_colour, $("#role-colorinput-regular").data("disabled")); + + // Loads the jquery sortable so users can manually sort roles + $("#rolesSelect").sortable({ + items: "a:not(.sort-disabled)", + update: function() { + $.ajax({ + url: $(this).data("url"), + type: 'PATCH', + data: $(this).sortable('serialize') + }); + } + }); + } } }); @@ -160,4 +173,35 @@ function loadColourSelectors() { location.reload() }); }) +} + +function loadRoleColourSelector(role_colour, disabled) { + if (!disabled) { + const pickrRoleRegular = new Pickr({ + el: '#role-colorinput-regular', + theme: 'monolith', + useAsButton: true, + lockOpacity: true, + defaultRepresentation: 'HEX', + closeWithKey: 'Enter', + default: role_colour, + + components: { + palette: true, + preview: true, + hue: true, + interaction: { + input: true, + save: true, + }, + }, + }); + + // On save update the colour input's background colour and update the role colour input + pickrRoleRegular.on("save", (color, instance) => { + $("#role-colorinput-regular").css("background-color", color.toHEXA().toString()); + $("#role-colorinput-regular").css("border-color", color.toHEXA().toString()); + $("#role-colour").val(color.toHEXA().toString()); + }); + } } \ No newline at end of file diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 9e2fd706bc..9767209a29 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -31,4 +31,6 @@ //= require tabler //= require tabler.plugins //= require jquery_ujs +//= require jquery-ui/widget +//= require jquery-ui/widgets/sortable //= require_tree . diff --git a/app/assets/javascripts/room.js.erb b/app/assets/javascripts/room.js.erb index 139b5ec732..2b0e8a931a 100644 --- a/app/assets/javascripts/room.js.erb +++ b/app/assets/javascripts/room.js.erb @@ -39,6 +39,11 @@ $(document).on('turbolinks:load', function(){ }, 2000) } }); + + // Forces the wrapper to take the entire screen height if the user can't create rooms + if ($("#cant-create-room-wrapper").length){ + $(".wrapper").css('height', '100%').css('height', '-=130px'); + } } // Display and update all fields related to creating a room in the createRoomModal diff --git a/app/assets/javascripts/user_edit.js b/app/assets/javascripts/user_edit.js new file mode 100644 index 0000000000..ecd0756f71 --- /dev/null +++ b/app/assets/javascripts/user_edit.js @@ -0,0 +1,88 @@ +// 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 . + +$(document).on('turbolinks:load', function(){ + var controller = $("body").data('controller'); + var action = $("body").data('action'); + if ((controller == "admins" && action == "edit_user") || (controller == "users" && action == "edit")) { + $(".setting-btn").click(function(data){ + var url = $("body").data("relative-root") + if (!url.endsWith("/")) { + url += "/" + } + url += "admins?setting=" + data.target.id + + window.location.href = url + }) + + // Clear the role when the user clicks the x + $(".clear-role").click(clearRole) + + // When the user selects an item in the dropdown add the role to the user + $("#role-select-dropdown").change(function(data){ + var dropdown = $("#role-select-dropdown"); + var select_role_id = dropdown.val(); + + if(select_role_id){ + // Disable the role in the dropdown + var selected_role = dropdown.find('[value=\"' + select_role_id + '\"]'); + selected_role.prop("disabled", true) + + // Add the role tag + var tag_container = $("#role-tag-container"); + tag_container.append("" + + selected_role.text() + ""); + + // Update the role ids input that gets submited on user update + var role_ids = $("#user_role_ids").val() + role_ids += " " + select_role_id + $("#user_role_ids").val(role_ids) + + // Add the clear role function to the tag + $("#user-role-tag_" + select_role_id).click(clearRole); + + // Reset the dropdown + dropdown.val(null) + } + }) + } +}) + +// This function removes the specfied role from a user +function clearRole(data){ + // Get the role id + var role_id = $(data.target).data("role-id"); + var role_tag = $("#user-role-tag_" + role_id); + + // Remove the role tag + $(role_tag).remove() + + // Update the role ids input + var role_ids = $("#user_role_ids").val() + var parsed_ids = role_ids.split(' ') + + var index = parsed_ids.indexOf(role_id.toString()); + + if (index > -1) { + parsed_ids.splice(index, 1); + } + + $("#user_role_ids").val(parsed_ids.join(' ')) + + // Enable the role in the role select dropdown + var selected_role = $("#role-select-dropdown").find('[value=\"' + role_id + '\"]'); + selected_role.prop("disabled", false) +} \ No newline at end of file diff --git a/app/assets/stylesheets/admins.scss b/app/assets/stylesheets/admins.scss index ee882a5d2f..6fe0260a46 100644 --- a/app/assets/stylesheets/admins.scss +++ b/app/assets/stylesheets/admins.scss @@ -54,4 +54,29 @@ height: 2rem; width: 2rem; } +} + +.sort-disabled{ + background: #e6e6e6 !important; + color: rgb(110, 118, 135) !important; + opacity: 0.75; + &:hover{ + opacity: 0.9; + } +} + +.form-disable{ + background-color: #e6e6e6; +} + +.role-colour-picker{ + color: white !important; +} + +.custom-role-tag{ + color: white !important; +} + +.user-role-tag{ + color: white !important; } \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 0b912ca4e5..2bda042ce1 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -32,6 +32,7 @@ @import "tabler/variables"; @import "bootstrap"; +@import "jquery-ui/sortable"; @import "tabler-custom"; @import "utilities/variables"; diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index 1daff6a91a..a0ec9aeece 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -18,3 +18,7 @@ // Place all the styles related to the Users controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ + +.user-role-tag{ + color: white !important; +} \ No newline at end of file diff --git a/app/assets/stylesheets/utilities/_primary_themes.scss b/app/assets/stylesheets/utilities/_primary_themes.scss index e94dc32808..b23f6db470 100644 --- a/app/assets/stylesheets/utilities/_primary_themes.scss +++ b/app/assets/stylesheets/utilities/_primary_themes.scss @@ -85,6 +85,11 @@ a { &:hover { color: $primary-color !important; background-color: $primary-color-lighten !important; + &.sort-disabled { + background: #e6e6e6 !important; + color: rgb(110, 118, 135) !important; + opacity: 0.9; + } } &:active { background-color: $primary-color-lighten !important; @@ -102,6 +107,12 @@ input:focus, select:focus { &, .list-group-item.active * { color: $primary-color !important; } + + &.sort-disabled { + background: #e6e6e6 !important; + color: rgb(110, 118, 135) !important; + opacity: 0.9 !important; + } } .text-primary { diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index ac06e954c6..7ee7cc4488 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -36,7 +36,8 @@ def index @search = params[:search] || "" @order_column = params[:column] && params[:direction] != "none" ? params[:column] : "created_at" @order_direction = params[:direction] && params[:direction] != "none" ? params[:direction] : "DESC" - @role = params[:role] || "" + + @role = params[:role] ? Role.find_by(name: params[:role], provider: @user_domain) : nil @pagy, @users = pagy(user_list) end @@ -64,24 +65,6 @@ def server_recordings def edit_user end - # POST /admins/promote/:user_uid - def promote - @user.add_role :admin - - send_user_promoted_email(@user) - - redirect_to admins_path, flash: { success: I18n.t("administrator.flash.promoted") } - end - - # POST /admins/demote/:user_uid - def demote - @user.remove_role :admin - - send_user_demoted_email(@user) - - redirect_to admins_path, flash: { success: I18n.t("administrator.flash.demoted") } - end - # POST /admins/ban/:user_uid def ban_user @user.roles = [] @@ -185,6 +168,158 @@ def default_recording_visibility } end + # ROLES + + # GET /admins/roles + def roles + @roles = Role.editable_roles(@user_domain) + + if @roles.count.zero? + Role.create_default_roles(@user_domain) + @roles = Role.editable_roles(@user_domain) + end + + @selected_role = if params[:selected_role].nil? + @roles.find_by(name: 'user') + else + @roles.find(params[:selected_role]) + end + end + + # POST /admin/role + # This method creates a new role scope to the users provider + def new_role + new_role_name = params[:role][:name] + + # Make sure that the role name isn't a duplicate or a reserved name like super_admin + if Role.duplicate_name(new_role_name, @user_domain) + flash[:alert] = I18n.t("administrator.roles.duplicate_name") + + return redirect_to admin_roles_path + end + + # Make sure the role name isn't empty + if new_role_name.strip.empty? + flash[:alert] = I18n.t("administrator.roles.empty_name") + + return redirect_to admin_roles_path + end + + # Create the new role with the second highest priority + # This means that it will only be more important than the user role + # This also updates the user role to have the highest priority + new_role = Role.create(name: new_role_name, provider: @user_domain) + user_role = Role.find_by(name: 'user', provider: @user_domain) + + new_role.priority = user_role.priority + user_role.priority += 1 + + new_role.save! + user_role.save! + + redirect_to admin_roles_path(selected_role: new_role.id) + end + + # PATCH /admin/roles/order + # This updates the priority of a site's roles + # Note: A lower priority role will always get used before a higher priority one + def change_role_order + user_role = Role.find_by(name: "user", provider: @user_domain) + admin_role = Role.find_by(name: "admin", provider: @user_domain) + + current_user_role = current_user.highest_priority_role + + # Users aren't allowed to update the priority of the admin or user roles + if params[:role].include?(user_role.id.to_s) || params[:role].include?(admin_role.id.to_s) + flash[:alert] = I18n.t("administrator.roles.invalid_order") + + return redirect_to admin_roles_path + end + + # Restrict users to only updating the priority for roles in their domain with a higher + # priority + params[:role].each do |id| + role = Role.find(id) + if role.priority <= current_user_role.priority || role.provider != @user_domain + flash[:alert] = I18n.t("administrator.roles.invalid_update") + return redirect_to admin_roles_path + end + end + + # Update the roles priority including the user role + top_priority = 0 + + params[:role].each_with_index do |id, index| + new_priority = index + [current_user_role.priority, 0].max + 1 + top_priority = new_priority + Role.where(id: id).update_all(priority: new_priority) + end + + user_role.priority = top_priority + 1 + user_role.save! + end + + # POST /admin/role/:role_id + # This method updates the permissions assigned to a role + def update_role + role = Role.find(params[:role_id]) + current_user_role = current_user.highest_priority_role + + # Checks that it is valid for the provider to update the role + if role.priority <= current_user_role.priority || role.provider != @user_domain + flash[:alert] = I18n.t("administrator.roles.invalid_update") + return redirect_to admin_roles_path(selected_role: role.id) + end + + 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, + :colour + ) + + # Make sure if the user is updating the role name that the role name is valid + if role.name != role_params[:name] && !Role.duplicate_name(role_params[:name], @user_domain) && + !role_params[:name].strip.empty? + role.name = role_params[:name] + elsif role.name != role_params[:name] + flash[:alert] = I18n.t("administrator.roles.duplicate_name") + + return redirect_to admin_roles_path(selected_role: role.id) + end + + role.update(permission_params) + + role.save! + + redirect_to admin_roles_path(selected_role: role.id) + end + + # DELETE admins/role/:role_id + # This deletes a role + def delete_role + role = Role.find(params[:role_id]) + + # Make sure no users are assigned to the role and the role isn't a reserved role + # before deleting + if role.users.count.positive? + flash[:alert] = I18n.t("administrator.roles.role_has_users", user_count: role.users.count) + return redirect_to admin_roles_path(selected_role: role.id) + elsif Role::RESERVED_ROLE_NAMES.include?(role) || role.provider != @user_domain || + role.priority <= current_user.highest_priority_role.priority + return redirect_to admin_roles_path(selected_role: role.id) + else + role.delete + end + + redirect_to admin_roles_path + end + private def find_user @@ -202,10 +337,10 @@ def verify_admin_of_user # Gets the list of users based on your configuration def user_list - initial_list = if current_user.has_cached_role? :super_admin - User.where.not(id: current_user.id).includes(:roles) + initial_list = if current_user.has_role? :super_admin + User.where.not(id: current_user.id) else - User.without_role(:super_admin).where.not(id: current_user.id).includes(:roles) + User.without_role(:super_admin).where.not(id: current_user.id) end if Rails.configuration.loadbalanced_configuration diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 80f2892b01..6d407183ae 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -137,7 +137,7 @@ def default_meeting_options # Checks to make sure that the admin has changed his password from the default def check_admin_password - if current_user&.has_cached_role?(:admin) && current_user&.greenlight_account? && + if current_user&.has_role?(:admin) && current_user&.greenlight_account? && current_user&.authenticate(Rails.configuration.admin_password_default) flash.now[:alert] = I18n.t("default_admin", @@ -185,10 +185,10 @@ def set_user_domain # Checks if the user is banned and logs him out if he is def check_user_role - if current_user&.has_cached_role? :denied + if current_user&.has_role? :denied session.delete(:user_id) redirect_to root_path, flash: { alert: I18n.t("registration.banned.fail") } - elsif current_user&.has_cached_role? :pending + elsif current_user&.has_role? :pending session.delete(:user_id) redirect_to root_path, flash: { alert: I18n.t("registration.approval.fail") } end diff --git a/app/controllers/concerns/emailer.rb b/app/controllers/concerns/emailer.rb index 4dc1457251..9146bb00a8 100644 --- a/app/controllers/concerns/emailer.rb +++ b/app/controllers/concerns/emailer.rb @@ -35,16 +35,16 @@ def send_password_reset_email(user) UserMailer.password_reset(@user, reset_link, logo_image, user_color).deliver_now end - def send_user_promoted_email(user) + def send_user_promoted_email(user, role) return unless Rails.configuration.enable_email_verification - UserMailer.user_promoted(user, root_url, logo_image, user_color).deliver_now + UserMailer.user_promoted(user, role, root_url, logo_image, user_color).deliver_now end - def send_user_demoted_email(user) + def send_user_demoted_email(user, role) return unless Rails.configuration.enable_email_verification - UserMailer.user_demoted(user, root_url, logo_image, user_color).deliver_now + UserMailer.user_demoted(user, role, root_url, logo_image, user_color).deliver_now end # Sends inivitation to join @@ -87,7 +87,7 @@ def user_verification_link end def admin_emails - admins = User.with_role(:admin) + admins = User.all_users_with_roles.where(roles: { can_manage_users: true }) if Rails.configuration.loadbalanced_configuration admins = admins.without_role(:super_admin) diff --git a/app/controllers/recordings_controller.rb b/app/controllers/recordings_controller.rb index 2c4446ae22..c82deff709 100644 --- a/app/controllers/recordings_controller.rb +++ b/app/controllers/recordings_controller.rb @@ -52,8 +52,8 @@ def find_room def verify_room_ownership if !current_user || !@room.owned_by?(current_user) || - !current_user.has_cached_role?(:admin) || - !current_user.has_cached_role?(:super_admin) + !current_user.has_role?(:admin) || + !current_user.has_role?(:super_admin) redirect_to root_path end end diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb index 233b6c23ca..06e3262d9b 100644 --- a/app/controllers/rooms_controller.rb +++ b/app/controllers/rooms_controller.rb @@ -24,8 +24,8 @@ class RoomsController < ApplicationController 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 - before_action :verify_room_ownership, except: [:create, :show, :join, :logout, :login] + before_action :find_room, except: [:create, :join_specific_room] + before_action :verify_room_ownership, except: [:create, :show, :join, :logout, :login, :join_specific_room] before_action :verify_room_owner_verified, only: [:show, :join], unless: -> { !Rails.configuration.enable_email_verification } before_action :verify_user_not_admin, only: [:show] @@ -60,11 +60,14 @@ def show @anyone_can_start = JSON.parse(@room[:room_settings])["anyoneCanStart"] if current_user && @room.owned_by?(current_user) - @search, @order_column, @order_direction, recs = - recordings(@room.bbb_id, @user_domain, params.permit(:search, :column, :direction), true) - - @pagy, @recordings = pagy_array(recs) + if current_user.highest_priority_role.can_create_rooms + @search, @order_column, @order_direction, recs = + recordings(@room.bbb_id, @user_domain, params.permit(:search, :column, :direction), true) + @pagy, @recordings = pagy_array(recs) + else + render :cant_create_rooms + end else # Get users name @name = if current_user @@ -138,6 +141,21 @@ def destroy redirect_to current_user.main_room end + # POST room/join + def join_specific_room + room_uid = params[:join_room][:url].split('/').last + + begin + @room = Room.find_by(uid: room_uid) + rescue ActiveRecord::RecordNotFound + return redirect_to current_user.main_room, alert: I18n.t("room.no_room.invalid_room_uid") + end + + return redirect_to current_user.main_room, alert: I18n.t("room.no_room.invalid_room_uid") if @room.nil? + + redirect_to room_path(@room) + end + # POST /:room_uid/start def start # Join the user in and start the meeting. @@ -275,7 +293,7 @@ def verify_room_owner_verified end def verify_user_not_admin - redirect_to admins_path if current_user && current_user&.has_cached_role?(:super_admin) + redirect_to admins_path if current_user && current_user&.has_role?(:super_admin) end def auth_required @@ -288,7 +306,7 @@ def room_limit_exceeded # Does not apply to admin # 15+ option is used as unlimited - return false if current_user&.has_cached_role?(:admin) || limit == 15 + return false if current_user&.has_role?(:admin) || limit == 15 current_user.rooms.count >= limit end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 779ed6f35a..c138cba8bf 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -144,12 +144,14 @@ def update errors.each { |k, v| @user.errors.add(k, v) } render :edit, params: { settings: params[:settings] } end - elsif user_params[:email] != @user.email && @user.update_attributes(user_params) + elsif user_params[:email] != @user.email && @user.update_attributes(user_params) && update_roles @user.update_attributes(email_verified: false) + flash[:success] = I18n.t("info_update_success") redirect_to redirect_path - elsif @user.update_attributes(user_params) + elsif @user.update_attributes(user_params) && update_roles update_locale(@user) + flash[:success] = I18n.t("info_update_success") redirect_to redirect_path else @@ -255,4 +257,65 @@ def passes_invite_reqs invitation[:present] end + + # Updates as user's roles + def update_roles + # Check that the user can edit roles + if current_user.highest_priority_role.can_edit_roles + new_roles = params[:user][:role_ids].split(' ').map(&:to_i) + old_roles = @user.roles.pluck(:id) + + added_role_ids = new_roles - old_roles + removed_role_ids = old_roles - new_roles + + added_roles = [] + removed_roles = [] + current_user_role = current_user.highest_priority_role + + # Check that the user has the permissions to add all the new roles + added_role_ids.each do |id| + role = Role.find(id) + + # Admins are able to add the admin role to other users. All other roles may only + # add roles with a higher priority + if (role.priority > current_user_role.priority || current_user_role.name == "admin") && + role.provider == @user_domain + added_roles << role + else + flash[:alert] = I18n.t("administrator.roles.invalid_assignment") + return false + end + end + + # Check that the user has the permissions to remove all the deleted roles + removed_role_ids.each do |id| + role = Role.find(id) + + # Admins are able to remove the admin role from other users. All other roles may only + # remove roles with a higher priority + if (role.priority > current_user_role.priority || current_user_role.name == "admin") && + role.provider == @user_domain + removed_roles << role + else + flash[:alert] = I18n.t("administrator.roles.invalid_removal") + return false + end + end + + # Send promoted/demoted emails + added_roles.each { |role| send_user_promoted_email(@user, role.name) if role.send_promoted_email } + removed_roles.each { |role| send_user_demoted_email(@user, role.name) if role.send_demoted_email } + + # Update the roles + @user.roles.delete(removed_roles) + @user.roles << added_roles + + # Make sure each user always has at least the user role + @user.roles = [Role.find_by(name: "user", provider: @user_domain)] if @user.roles.count.zero? + + @user.save! + else + true + end + end end diff --git a/app/helpers/admins_helper.rb b/app/helpers/admins_helper.rb index 704afb9129..c517c9f664 100644 --- a/app/helpers/admins_helper.rb +++ b/app/helpers/admins_helper.rb @@ -78,4 +78,8 @@ def registration_method_string def room_limit_number Setting.find_or_create_by!(provider: user_settings_provider).get_value("Room Limit").to_i end + + def edit_disabled + @edit_disabled ||= @selected_role.priority <= current_user.highest_priority_role.priority + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c71965df00..e8136084de 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -107,7 +107,25 @@ def current_translations # Returns the page that the logo redirects to when clicked on def home_page return root_path unless current_user - return admins_path if current_user.has_cached_role? :super_admin + return admins_path if current_user.has_role? :super_admin current_user.main_room end + + def role_colour(role) + role.colour || Rails.configuration.primary_color_default + end + + def translated_role_name(role) + if role.name == "denied" + I18n.t("roles.banned") + elsif role.name == "pending" + I18n.t("roles.pending") + elsif role.name == "admin" + I18n.t("roles.admin") + elsif role.name == "user" + I18n.t("roles.user") + else + role.name + end + end end diff --git a/app/helpers/rooms_helper.rb b/app/helpers/rooms_helper.rb index 26bfaf6587..009fd77d94 100644 --- a/app/helpers/rooms_helper.rb +++ b/app/helpers/rooms_helper.rb @@ -37,7 +37,7 @@ def room_limit_exceeded # Does not apply to admin or users that aren't signed in # 15+ option is used as unlimited - return false if current_user&.has_cached_role?(:admin) || limit == 15 + return false if current_user&.has_role?(:admin) || limit == 15 current_user.rooms.length >= limit end @@ -46,7 +46,7 @@ def current_room_exceeds_limit(room) # Get how many rooms need to be deleted to reach allowed room number limit = Setting.find_or_create_by!(provider: user_settings_provider).get_value("Room Limit").to_i - return false if current_user&.has_cached_role?(:admin) || limit == 15 + return false if current_user&.has_role?(:admin) || limit == 15 @diff = current_user.rooms.count - limit @diff.positive? && current_user.rooms.pluck(:id).index(room.id) + 1 > limit diff --git a/app/helpers/theming_helper.rb b/app/helpers/theming_helper.rb index 054799805c..e3f073fbba 100644 --- a/app/helpers/theming_helper.rb +++ b/app/helpers/theming_helper.rb @@ -31,7 +31,7 @@ def user_color # Returns the user's provider in the settings context def user_settings_provider - if Rails.configuration.loadbalanced_configuration && current_user && !current_user&.has_cached_role?(:super_admin) + if Rails.configuration.loadbalanced_configuration && current_user && !current_user&.has_role?(:super_admin) current_user.provider elsif Rails.configuration.loadbalanced_configuration @user_domain diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index e93f83e2c4..c62f3d7e68 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -20,4 +20,20 @@ module UsersHelper def recaptcha_enabled? Rails.configuration.recaptcha_enabled end + + def disabled_roles(user) + current_user_role = current_user.highest_priority_role + + # Admins are able to remove the admin role from other admins + # For all other roles they can only add/remove roles with a higher priority + disallowed_roles = if current_user_role.name == "admin" + Role.editable_roles(@user_domain).where("priority < #{current_user_role.priority}") + .pluck(:id) + else + Role.editable_roles(@user_domain).where("priority <= #{current_user_role.priority}") + .pluck(:id) + end + + user.roles.by_priority.pluck(:id) | disallowed_roles + end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index cebebe9289..b080fd83cf 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -35,20 +35,22 @@ def password_reset(user, url, image, color) mail to: user.email, subject: t('reset_password.subtitle') end - def user_promoted(user, url, image, color) + def user_promoted(user, role, url, image, color) @url = url @admin_url = url + "admins" @image = image @color = color - mail to: user.email, subject: t('mailer.user.promoted.subtitle') + @role = role + mail to: user.email, subject: t('mailer.user.promoted.subtitle', role: role) end - def user_demoted(user, url, image, color) + def user_demoted(user, role, url, image, color) @url = url @root_url = url @image = image @color = color - mail to: user.email, subject: t('mailer.user.demoted.subtitle') + @role = role + mail to: user.email, subject: t('mailer.user.demoted.subtitle', role: role) end def invite_email(name, email, url, image, color) diff --git a/app/models/ability.rb b/app/models/ability.rb index 07ec83584a..d06263c932 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -24,10 +24,25 @@ def initialize(user) cannot :manage, AdminsController elsif user.has_role? :super_admin can :manage, :all - elsif user.has_role? :admin - can :manage, :all - elsif user.has_role? :user - cannot :manage, AdminsController + else + highest_role = user.highest_priority_role + if highest_role.can_edit_site_settings + can [:index, :site_settings, :server_recordings, :branding, :coloring, :coloring_lighten, :coloring_darken, + :room_authentication, :registration_method, :room_limit, :default_recording_visibility], :admin + end + + if highest_role.can_edit_roles + can [:index, :roles, :new_role, :change_role_order, :update_role, :delete_role], :admin + end + + if highest_role.can_manage_users + can [:index, :roles, :edit_user, :promote, :demote, :ban_user, :unban_user, + :approve, :invite], :admin + end + + if !highest_role.can_edit_site_settings && !highest_role.can_edit_roles && !highest_role.can_manage_users + cannot :manage, AdminsController + end end end end diff --git a/app/models/role.rb b/app/models/role.rb index 856b92c682..a178fad4be 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -19,13 +19,25 @@ class Role < ApplicationRecord has_and_belongs_to_many :users, join_table: :users_roles - belongs_to :resource, - polymorphic: true, - optional: true + default_scope { order(:priority) } + scope :by_priority, -> { order(:priority) } + scope :editable_roles, ->(provider) { where(provider: provider).where.not(name: %w[super_admin denied pending]) } - validates :resource_type, - inclusion: { in: Rolify.resource_types }, - allow_nil: true + RESERVED_ROLE_NAMES = %w[super_admin admin pending denied user] - scopify + def self.duplicate_name(name, provider) + RESERVED_ROLE_NAMES.include?(name) || Role.exists?(name: name, provider: provider) + end + + def self.create_default_roles(provider) + Role.create(name: "user", provider: provider, priority: 1, can_create_rooms: true, colour: "#868e96") + Role.create(name: "admin", provider: provider, priority: 0, 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, colour: "#f1c40f") + Role.create(name: "pending", provider: provider, priority: -1, colour: "#17a2b8") + Role.create(name: "denied", provider: provider, priority: -1, colour: "#343a40") + Role.create(name: "super_admin", provider: provider, priority: -2, 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, colour: "#cd201f") + end end diff --git a/app/models/user.rb b/app/models/user.rb index f3426fe256..9f2a65b821 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,7 +19,6 @@ require 'bbb_api' class User < ApplicationRecord - rolify include ::BbbApi attr_accessor :reset_token @@ -33,6 +32,8 @@ class User < ApplicationRecord has_many :rooms belongs_to :main_room, class_name: 'Room', foreign_key: :room_id, required: false + has_and_belongs_to_many :roles, join_table: :users_roles + validates :name, length: { maximum: 256 }, presence: true validates :provider, presence: true validate :check_if_email_can_be_blank @@ -59,6 +60,7 @@ def from_omniauth(auth) u.username = auth_username(auth) unless u.username u.email = auth_email(auth) u.image = auth_image(auth) unless u.image + auth_roles(u, auth) u.email_verified = true u.save! end @@ -99,6 +101,18 @@ def auth_image(auth) auth['info']['image'] end end + + def auth_roles(user, auth) + unless auth['info']['roles'].nil? + roles = auth['info']['roles'].split(',') + + role_provider = auth['provider'] == "bn_launcher" ? auth['info']['customer'] : "greenlight" + roles.each do |role_name| + role = Role.where(provider: role_provider, name: role_name).first + user.roles << role unless role.nil? + end + end + end end def self.admins_search(string, role) @@ -112,16 +126,16 @@ def self.admins_search(string, role) search_query = "" role_search_param = "" - if role.present? - search_query = "(users.name LIKE :search OR email LIKE :search OR username LIKE :search" \ - " OR users.#{created_at_query} LIKE :search OR provider LIKE :search)" \ - " AND roles.name = :roles_search" - role_search_param = role - else + if role.nil? search_query = "users.name LIKE :search OR email LIKE :search OR username LIKE :search" \ - " OR users.#{created_at_query} LIKE :search OR provider LIKE :search" \ + " OR users.#{created_at_query} LIKE :search OR users.provider LIKE :search" \ " OR roles.name LIKE :roles_search" - role_search_param = "%#{string}%".downcase + role_search_param = "%#{string}%" + else + search_query = "(users.name LIKE :search OR email LIKE :search OR username LIKE :search" \ + " OR users.#{created_at_query} LIKE :search OR users.provider LIKE :search)" \ + " AND roles.name = :roles_search" + role_search_param = role.name end search_param = "%#{string}%" @@ -204,17 +218,14 @@ def activation_token def admin_of?(user) if Rails.configuration.loadbalanced_configuration - # Pulls in the user roles if they weren't request in the original request - # So the has_cached_role? doesn't always return false - user.roles - if has_cached_role? :super_admin + if has_role? :super_admin id != user.id else - (has_cached_role? :admin) && (id != user.id) && (provider == user.provider) && - (!user.has_cached_role? :super_admin) + highest_priority_role.can_manage_users && (id != user.id) && (provider == user.provider) && + (!user.has_role? :super_admin) end else - ((has_cached_role? :admin) || (has_cached_role? :super_admin)) && (id != user.id) + (highest_priority_role.can_manage_users || (has_role? :super_admin)) && (id != user.id) end end @@ -228,6 +239,50 @@ def self.new_token SecureRandom.urlsafe_base64 end + # role functions + def highest_priority_role + roles.by_priority.first + end + + def add_role(role) + unless has_role?(role) + role_provider = Rails.configuration.loadbalanced_configuration ? provider : "greenlight" + + roles << Role.find_or_create_by(name: role, provider: role_provider) + + save! + end + end + + def remove_role(role) + if has_role?(role) + role_provider = Rails.configuration.loadbalanced_configuration ? provider : "greenlight" + + roles.delete(Role.find_by(name: role, provider: role_provider)) + save! + end + end + + # This rule is disabled as the function name must be has_role? + # rubocop:disable Naming/PredicateName + def has_role?(role) + # rubocop:enable Naming/PredicateName + roles.exists?(name: role) + end + + def self.with_role(role) + User.all_users_with_roles.where(roles: { name: role }) + end + + def self.without_role(role) + User.where.not(id: with_role(role).pluck(:id)) + 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") + end + private def create_reset_activation_digest(token) @@ -251,6 +306,10 @@ def initialize_main_room # Initialize the user to use the default user role def assign_default_role + role_provider = Rails.configuration.loadbalanced_configuration ? provider : "greenlight" + + Role.create_default_roles(role_provider) if Role.where(provider: role_provider).count.zero? + add_role(:user) if roles.blank? end diff --git a/app/views/admins/components/_menu_buttons.html.erb b/app/views/admins/components/_menu_buttons.html.erb index 7dfcc8ffc6..395cc3d439 100644 --- a/app/views/admins/components/_menu_buttons.html.erb +++ b/app/views/admins/components/_menu_buttons.html.erb @@ -14,13 +14,24 @@ %>
- <%= link_to admins_path, class: "list-group-item list-group-item-action dropdown-item #{"active" if active_page == "index"}" do %> - <%= t("administrator.users.title") %> + <% highest_role = current_user.highest_priority_role %> + <% highest_role.name %> + <% if highest_role.can_manage_users || highest_role.name == "super_admin" %> + <%= link_to admins_path, class: "list-group-item list-group-item-action dropdown-item #{"active" if active_page == "index"}" do %> + <%= t("administrator.users.title") %> + <% end %> <% end %> - <%= link_to admin_site_settings_path, class: "list-group-item list-group-item-action dropdown-item #{"active" if active_page == "site_settings"}" do %> - <%= t("administrator.site_settings.title") %> + <% if highest_role.can_edit_site_settings || highest_role.name == "super_admin" %> + <%= link_to admin_recordings_path, class: "list-group-item list-group-item-action dropdown-item #{"active" if active_page == "server_recordings"}" do %> + <%= t("administrator.recordings.title") %> + <% end %> + <%= link_to admin_site_settings_path, class: "list-group-item list-group-item-action dropdown-item #{"active" if active_page == "site_settings"}" do %> + <%= t("administrator.site_settings.title") %> + <% end %> <% end %> - <%= link_to admin_recordings_path, class: "list-group-item list-group-item-action dropdown-item #{"active" if active_page == "server_recordings"}" do %> - <%= t("administrator.recordings.title") %> + <% if highest_role.can_edit_roles || highest_role.name == "super_admin" %> + <%= link_to admin_roles_path, class: "list-group-item list-group-item-action dropdown-item #{"active" if active_page == "roles"}" do %> + <%= t("administrator.roles.title") %> + <% end %> <% end %>
\ No newline at end of file diff --git a/app/views/admins/components/_roles.html.erb b/app/views/admins/components/_roles.html.erb new file mode 100644 index 0000000000..b88548cd56 --- /dev/null +++ b/app/views/admins/components/_roles.html.erb @@ -0,0 +1,94 @@ +<% +# 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 . +%> + +
+
+
+
+
+ <% @roles.each do |role| %> + <%= link_to admin_roles_path(selected_role: role.id), + class: "#{"sort-disabled" if role.name == "user" || role.name == "admin" || role.priority <= current_user.highest_priority_role.priority } dropdown-item list-group-item list-group-item-action #{"active" if @selected_role.id == role.id}", + id: dom_id(role) do %> + <%= translated_role_name(role) %> + <% end %> + <% end %> +
+ <%= link_to "#", id: "newRoleButton", class: "list-group-item list-group-item-action", "data-toggle" => "modal", "data-target" => '#createRoleModal' do %> + <%= t("administrator.roles.new_role") %> + <% end %> +
+
+
"> + <%= form_for(@selected_role, url: admin_update_role_path(@selected_role.id), method: :post) do |f| %> + <%= f.label t('administrator.roles.name'), class: "form-label" %> + <%= f.text_field :name, class: 'form-control mb-3', value: translated_role_name(@selected_role), readonly: edit_disabled || @selected_role.name == "user" || @selected_role.name == "admin", required: true %> + + <%= f.hidden_field :colour, id: "role-colour", value: role_colour(@selected_role) %> +
+ + +
+
+ <%= t("administrator.site_settings.color.regular") %> +
+
+
+ + + + + + + + +
+ <%= f.submit t("update"), class: "btn btn-primary float-right ml-2 mb-2", disabled: edit_disabled %> + <% if @selected_role.name != "user" && @selected_role.name != "admin" && !edit_disabled %> + <%= link_to admin_delete_role_path(@selected_role.id), method: :delete, class: "float-right btn btn-danger" do %> + <%= t("administrator.roles.delete") %> + <% end %> + <% end %> +
+ <% end %> +
+
+
+ +<%= render "shared/modals/create_role_modal" %> \ No newline at end of file diff --git a/app/views/admins/components/_users.html.erb b/app/views/admins/components/_users.html.erb index 9ceaa91308..2194623218 100644 --- a/app/views/admins/components/_users.html.erb +++ b/app/views/admins/components/_users.html.erb @@ -28,7 +28,7 @@ # with BigBlueButton; if not, see . %> -<% if @role.present? %> +<% unless @role.nil? %> <%= render "shared/components/admins_tags" %> <% end %> @@ -88,7 +88,7 @@ <%= user.provider %> <% roles = user.roles().pluck(:name) %> - <%= render "shared/components/admins_role", roles: roles %> + <%= render "shared/components/admins_role", role: user.highest_priority_role %> <% if roles.include?("pending") %> @@ -122,16 +122,6 @@ - - <% if roles.include?("admin") %> - <%= button_to admin_demote_path(user_uid: user.uid), class: "dropdown-item" do %> - <%= t("administrator.users.settings.demote") %> - <% end %> - <% elsif roles.include?("user") %> - <%= button_to admin_promote_path(user_uid: user.uid), class: "dropdown-item" do %> - <%= t("administrator.users.settings.promote") %> - <% end %> - <% end %> <%= button_to admin_ban_path(user_uid: user.uid), class: "dropdown-item" do %> <%= t("administrator.users.settings.ban") %> <% end %> diff --git a/app/views/admins/roles.html.erb b/app/views/admins/roles.html.erb new file mode 100644 index 0000000000..9ab84f5f54 --- /dev/null +++ b/app/views/admins/roles.html.erb @@ -0,0 +1,27 @@ +<% +# 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 . +%> + +
+ <%= render "shared/components/subtitle", subtitle: t("administrator.title"), search: false %> + +
+
+ <%= render "admins/components/menu_buttons" %> +
+
+ <%= render "admins/components/setting_view", setting_id: "roles", setting_title: t("administrator.roles.title"), search: false %> +
+
+
diff --git a/app/views/rooms/cant_create_rooms.html.erb b/app/views/rooms/cant_create_rooms.html.erb new file mode 100644 index 0000000000..f3dbc98da4 --- /dev/null +++ b/app/views/rooms/cant_create_rooms.html.erb @@ -0,0 +1,44 @@ + + +<% +# 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 . +%> + +
+
+
+
+
+
+
+

<%= t("room.no_room.title") %>

+
+
+ <%= form_for(:join_room, url: join_room_path) do |f| %> +
+ + + + <%= f.text_field :url, class: "form-control", value: "", placeholder: t("room.no_room.placeholder"), required: "" %> +
+
+ <%= f.submit t("room.join"), class:"btn btn-primary btn-block" %> +
+ <% end %> +
+
+
+
+
+
\ No newline at end of file diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb index 3182142faa..9595ec0ea7 100755 --- a/app/views/shared/_header.html.erb +++ b/app/views/shared/_header.html.erb @@ -23,7 +23,7 @@
<% if current_user %> - <% if current_user.has_cached_role? :super_admin %> + <% if current_user.has_role? :super_admin %> <% admins_page = params[:controller] == "admins" && params[:action] == "index" ? "active" : "" %> <%= link_to admins_path, class: "px-3 mx-1 mt-1 header-nav #{admins_page}" do %> <%= t("header.dropdown.home") %> @@ -34,9 +34,11 @@ <%= t("header.dropdown.home") %> <% end %> - <% all_rec_page = params[:controller] == "users" && params[:action] == "recordings" ? "active" : "" %> - <%= link_to get_user_recordings_path(current_user), class: "px-3 mx-1 mt-1 header-nav #{all_rec_page}" do %> - <%= t("header.all_recordings") %> + <% if current_user.highest_priority_role.can_create_rooms %> + <% all_rec_page = params[:controller] == "users" && params[:action] == "recordings" ? "active" : "" %> + <%= link_to get_user_recordings_path(current_user), class: "px-3 mx-1 mt-1 header-nav #{all_rec_page}" do %> + <%= t("header.all_recordings") %> + <% end %> <% end %> <% end %> @@ -56,10 +58,19 @@ <%= link_to edit_user_path(current_user), class: "dropdown-item" do %> <%= t("header.dropdown.settings") %> <% end %> - <% if current_user.has_cached_role? :admin %> + <% highest_role = current_user.highest_priority_role %> + <% if highest_role.can_manage_users || highest_role.name == "super_admin" %> <%= link_to admins_path, class: "dropdown-item" do %> <%= t("header.dropdown.account_settings") %> <% end %> + <% elsif highest_role.can_edit_site_settings %> + <%= link_to admin_site_settings_path, class: "dropdown-item" do %> + <%= t("header.dropdown.account_settings") %> + <% end %> + <% elsif highest_role.can_edit_roles%> + <%= link_to admin_roles_path, class: "dropdown-item" do %> + <%= t("header.dropdown.account_settings") %> + <% end %> <% end %> diff --git a/app/views/shared/components/_admins_role.html.erb b/app/views/shared/components/_admins_role.html.erb index baa42abbf8..fdc830dee8 100644 --- a/app/views/shared/components/_admins_role.html.erb +++ b/app/views/shared/components/_admins_role.html.erb @@ -13,24 +13,6 @@ # with BigBlueButton; if not, see . %> -<% if roles.include?("denied")%> - -<% elsif roles.include?("pending") %> - -<% elsif roles.include?("super_admin") %> - -<% elsif roles.include?("admin") %> - -<% else %> - -<% end %> \ No newline at end of file + \ No newline at end of file diff --git a/app/views/shared/components/_admins_tags.html.erb b/app/views/shared/components/_admins_tags.html.erb index a1e6142d1b..27f46b4073 100644 --- a/app/views/shared/components/_admins_tags.html.erb +++ b/app/views/shared/components/_admins_tags.html.erb @@ -16,42 +16,12 @@
- <% if @role == "denied"%> - - <%= t("roles.banned") %> - - - - - <% elsif @role == "pending" %> - - <%= t("roles.pending") %> - - - - - <% elsif @role == "super_admin" %> - - <%= t("roles.super_admin") %> - - - - - <% elsif @role == "admin" %> - - <%= t("roles.administrator") %> - - - - - <% else %> - - <%= t("roles.user") %> - - - - - <% end %> + " class="tag custom-role-tag"> + <%= translated_role_name(@role) %> + + + +
\ No newline at end of file diff --git a/app/views/shared/modals/_create_role_modal.html.erb b/app/views/shared/modals/_create_role_modal.html.erb new file mode 100644 index 0000000000..c2e504ff06 --- /dev/null +++ b/app/views/shared/modals/_create_role_modal.html.erb @@ -0,0 +1,44 @@ +<% +# 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 . +%> + + diff --git a/app/views/shared/settings/_account.html.erb b/app/views/shared/settings/_account.html.erb index 8d09d1c88f..f0b579e1a9 100644 --- a/app/views/shared/settings/_account.html.erb +++ b/app/views/shared/settings/_account.html.erb @@ -38,6 +38,28 @@ <%= f.label t("settings.account.language"), class: "form-label" %> <%= f.select :language, language_options, {}, { class: "form-control custom-select" } %> + <% current_user_role = current_user.highest_priority_role %> +
+
+ <%= f.label t("settings.account.roles"), class: "form-label" %> +
+ <% @user.roles.by_priority.each do |role| %> + " style="<%= "background-color: #{role_colour(role)};border-color: #{role_colour(role)};" %>" class="tag user-role-tag"> + <%= translated_role_name(role) %> + <% if (current_user_role.can_edit_roles || current_user_role.name == "super_admin") && (role.priority > current_user_role.priority || current_user_role.name == "admin") %> + + + + <% end %> + + <% end %> +
+ <% if current_user_role.can_edit_roles || current_user_role.name == "super_admin" %> + <% provider = Rails.configuration.loadbalanced_configuration ? current_user.provider : "greenlight" %> + <%= f.select :roles, Role.editable_roles(@user_domain).map{|role| [translated_role_name(role), role.id, {'data-colour' => role_colour(role)}]}.unshift(["", nil, {'data-colour' => nil}]), {disabled: disabled_roles(@user)}, { class: "form-control custom-select", id: "role-select-dropdown" } %> + <% end %> + <%= f.hidden_field :role_ids, id: "user_role_ids", value: @user.roles.by_priority.pluck(:id) %> + <%= f.label t("settings.account.image"), class: "form-label mt-5" %>
diff --git a/app/views/user_mailer/user_demoted.html.erb b/app/views/user_mailer/user_demoted.html.erb index 21c18b937d..1732bf7e74 100644 --- a/app/views/user_mailer/user_demoted.html.erb +++ b/app/views/user_mailer/user_demoted.html.erb @@ -21,11 +21,11 @@ <%= image_tag(@image, height: '70') %>

- <%= t('mailer.user.demoted.subtitle') %> + <%= t('mailer.user.demoted.subtitle', role: @role) %>

- <%= t('mailer.user.demoted.info', url: @url) %> + <%= t('mailer.user.demoted.info', url: @url, role: @role) %>

diff --git a/app/views/user_mailer/user_demoted.text.erb b/app/views/user_mailer/user_demoted.text.erb index 275ddb7d88..1ed5841092 100644 --- a/app/views/user_mailer/user_demoted.text.erb +++ b/app/views/user_mailer/user_demoted.text.erb @@ -17,9 +17,9 @@ %> -<%= t('mailer.user.demoted.subtitle') %> +<%= t('mailer.user.demoted.subtitle', role: @role) %> -<%= t('mailer.user.demoted.info', url: @url) %> +<%= t('mailer.user.demoted.info', url: @url, role: @role) %> <%= t('mailer.user.demoted.more-info') %> diff --git a/app/views/user_mailer/user_promoted.html.erb b/app/views/user_mailer/user_promoted.html.erb index 9121af0a84..02b00c1fad 100644 --- a/app/views/user_mailer/user_promoted.html.erb +++ b/app/views/user_mailer/user_promoted.html.erb @@ -21,15 +21,15 @@ <%= image_tag(@image, height: '70') %>

- <%= t('mailer.user.promoted.subtitle') %> + <%= t('mailer.user.promoted.subtitle', role: @role) %>

- <%= t('mailer.user.promoted.info', url: @url) %> + <%= t('mailer.user.promoted.info', url: @url, role: @role) %>

- <%= t('mailer.user.promoted.more-info') %> + <%= t('mailer.user.promoted.more-info', url: @url) %>

diff --git a/app/views/user_mailer/user_promoted.text.erb b/app/views/user_mailer/user_promoted.text.erb index f6af8fdeb1..4db9784404 100644 --- a/app/views/user_mailer/user_promoted.text.erb +++ b/app/views/user_mailer/user_promoted.text.erb @@ -17,10 +17,10 @@ %> -<%= t('mailer.user.promoted.subtitle') %> +<%= t('mailer.user.promoted.subtitle', role: @role) %> -<%= t('mailer.user.promoted.info', url: @url) %> +<%= t('mailer.user.promoted.info', url: @url, role: @role) %> -<%= t('mailer.user.promoted.more-info') %> +<%= t('mailer.user.promoted.more-info', url: @url) %> <%= @admin_url %> diff --git a/config/initializers/rolify.rb b/config/initializers/rolify.rb deleted file mode 100644 index ace6ec8d97..0000000000 --- a/config/initializers/rolify.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -Rolify.configure do |config| - # By default ORM adapter is ActiveRecord. uncomment to use mongoid - # config.use_mongoid - - # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false - # config.use_dynamic_shortcuts - - # Configuration to remove roles from database once the last resource is removed. Default is: true - # config.remove_role_if_empty = false -end diff --git a/config/locales/en.yml b/config/locales/en.yml index 2fa42b0008..de9760ac45 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -80,6 +80,27 @@ en: recordings: title: Server Recordings no_recordings: This server has no recordings. + roles: + can_create_rooms: Can create rooms + delete: Delete the role + duplicate_name: The role name you provided was a duplicate. All role names must be unique + empty_name: All roles must have a non empty name + invalid_order: The user role must be the lowest priority role and the admin role must be the highest priority role + invalid_update: You can't update a role with a higher priority than your own role + name: Role Name + new_role: Create a new role + role_has_users: This role is assigned to %{user_count} accounts. Please remove all accounts from this role before deleting it. + title: Roles + promote_email: Send an email to users when they are assigned this role + demote_email: Send an email to users when they are removed from this role + edit_site_settings: Allow users with this role to edit site settings + edit_roles: Allow users with this role to edit other roles + manage_users: Allow users with this role to manage users + invalid_assignment: You can't assign a role with a higher priority than your own to a user + invalid_removal: You can't remove a role with a higher priority than your own + colour: + title: Role Colour + info: Set the colour that will be associated with the role title: Organization Settings users: invite: Invite User @@ -90,9 +111,8 @@ en: decline: Decline ban: Ban User delete: Delete - demote: Demote to User edit: Edit - promote: Promote to Admin + edit_roles: Edit the user roles unban: Unban User table: authenticator: Authenticator @@ -226,10 +246,10 @@ en: subject: Account Approved username: Your username is %{email}. demoted: - info: You are no longer an administrator on %{url}. - more-info: You now have the same privileges as a regular user and will no longer be able to access any of the Administrator settings. + info: You are no longer an %{role} on %{url}. + more-info: You now have the same privileges as a regular user. root_link: Sign In - subtitle: Administrator Rights Rescinded + subtitle: "%{role} Rights Rescinded" invite: info: You have been invited to your own personal space by %{name} signup_info: To signup using your email, click the button below and follow the steps. @@ -250,9 +270,9 @@ en: ignore: You can safely ignore this email if you did not request a password reset. promoted: admins_link: Visit the Organization Page - info: You are now an administrator on %{url}. - more-info: As an administrator, you can manage users, their role and configure your site settings by changing the logo and brand colour. - subtitle: Administrator Rights Granted + info: You are now an %{role} on %{url}. + more-info: To view your new abilities please visit %{url}. + subtitle: "%{role} Rights Granted" verify_email: welcome: Welcome to your personal space, %{name}! success: Leveraging %{bigbluebutton}, you can create your own rooms to host sessions and collaborate with others. @@ -263,6 +283,12 @@ en: thanks: Thanks for joining and have a great day! max_concurrent: The maximum number of concurrent sessions allowed has been reached! modal: + create_role: + create: Create a new Role + footer_text: You can edit the individual permissions for this role after you've created it + name_placeholder: Enter a role name... + not_blank: Role name cannot be blank. + title: Create New Role create_room: access_code: Access Code access_code_placeholder: Generate an optional room access code @@ -369,10 +395,9 @@ en: confirm: New Password Confirmation update: Update Password roles: - administrator: Admin + admin: Admin banned: Banned pending: Pending - super_admin: Super Admin user: User room: access_code_required: Please enter a valid access code to join the room @@ -386,6 +411,10 @@ en: last_session: Last session on %{session} login: Enter owner: Owner + no_room: + title: Join a Room + placeholder: Enter the room url or the room id for the room you want to join. + invalid_room_uid: The room url/uid you entered was invalid. no_sessions: This room has no sessions, yet! recordings: Room Recordings room_limit: You have reached the maximum number of rooms allowed @@ -406,6 +435,7 @@ en: provider: Provider image: Image image_url: Profile Image URL + roles: User Roles subtitle: Update your Account Info title: Account Info delete: diff --git a/config/routes.rb b/config/routes.rb index 445103d940..4d09d0d5e8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,8 +46,6 @@ post '/coloring_darken', to: 'admins#coloring_darken', as: :admin_coloring_darken post '/signup', to: 'admins#signup', as: :admin_signup get '/edit/:user_uid', to: 'admins#edit_user', as: :admin_edit_user - post '/promote/:user_uid', to: 'admins#promote', as: :admin_promote - post '/demote/:user_uid', to: 'admins#demote', as: :admin_demote post '/ban/:user_uid', to: 'admins#ban_user', as: :admin_ban post '/unban/:user_uid', to: 'admins#unban_user', as: :admin_unban post '/invite', to: 'admins#invite', as: :invite_user @@ -55,6 +53,11 @@ post '/approve/:user_uid', to: 'admins#approve', as: :admin_approve post '/room_limit', to: 'admins#room_limit', as: :admin_room_limit post '/default_recording_visibility', to: 'admins#default_recording_visibility', as: :admin_recording_visibility + get '/roles', to: 'admins#roles', as: :admin_roles + post '/role', to: 'admins#new_role', as: :admin_new_role + patch 'roles/order', to: 'admins#change_role_order', as: :admin_roles_order + post '/role/:role_id', to: 'admins#update_role', as: :admin_update_role + delete 'role/:role_id', to: 'admins#delete_role', as: :admin_delete_role end scope '/themes' do @@ -96,6 +99,9 @@ # Room resources. resources :rooms, only: [:create, :show, :destroy], param: :room_uid, path: '/' + # Join a room by UID + post '/room/join', to: 'rooms#join_specific_room', as: :join_room + # Extended room routes. scope '/:room_uid' do post '/', to: 'rooms#join' diff --git a/db/migrate/20190726153012_add_custom_roles.rb b/db/migrate/20190726153012_add_custom_roles.rb new file mode 100644 index 0000000000..dfe5d78335 --- /dev/null +++ b/db/migrate/20190726153012_add_custom_roles.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +class AddCustomRoles < ActiveRecord::Migration[5.2] + def up + super_admin_id = -1 + user_id = -1 + admin_id = -1 + denied_id = -1 + pending_id = -1 + + old_roles = ActiveRecord::Base.connection.execute("select * from roles") + + # Determine what ids corresponded to what roles in the old table + old_roles.each do |role| + if role["name"] == "super_admin" + super_admin_id = role["id"] + elsif role["name"] == "user" + user_id = role["id"] + elsif role["name"] == "admin" + admin_id = role["id"] + elsif role["name"] == "denied" + denied_id = role["id"] + elsif role["name"] == "pending" + pending_id = role["id"] + end + end + + # Replace Rolify's table with our own + drop_table :roles + + create_table(:roles) do |t| + t.string :name + t.integer :priority, default: 9999 + t.boolean :can_create_rooms, default: false + t.boolean :send_promoted_email, default: false + t.boolean :send_demoted_email, default: false + t.boolean :can_edit_site_settings, default: false + t.boolean :can_edit_roles, default: false + t.boolean :can_manage_users, default: false + t.string :colour + t.string :provider + + t.timestamps + end + + add_index(:roles, :name) + add_index(:roles, [:name, :provider], unique: true) + + # Look at all the old role assignments and and for each role create a new role + # that is scoped to the provider + old_assignments = ActiveRecord::Base.connection.execute("select * from users_roles") + new_assignments = [] + + old_assignments.each do |assignment| + user = User.find(assignment["user_id"]) + new_assignment = { "user_id" => assignment["user_id"] } + if assignment["role_id"] == super_admin_id + new_assignment["new_role_id"] = generate_scoped_role(user, "super_admin") + elsif assignment["role_id"] == user_id + new_assignment["new_role_id"] = generate_scoped_role(user, "user") + elsif assignment["role_id"] == admin_id + new_assignment["new_role_id"] = generate_scoped_role(user, "admin") + elsif assignment["role_id"] == denied_id + new_assignment["new_role_id"] = generate_scoped_role(user, "denied") + elsif assignment["role_id"] == pending_id + new_assignment["new_role_id"] = generate_scoped_role(user, "pending") + end + + new_assignments << new_assignment + end + + assign_new_users(new_assignments) + end + + def generate_scoped_role(user, role_name) + provider = Rails.configuration.loadbalanced_configuration ? user.provider : 'greenlight' + new_role = Role.find_by(name: role_name, provider: provider) + + if new_role.nil? + Role.create_default_roles(provider) + + new_role = Role.find_by(name: role_name, provider: provider) + end + + new_role.id + end + + def assign_new_users(new_assignments) + # Delete the old assignments + ActiveRecord::Base.connection.execute("DELETE FROM users_roles") + # Add the role assignments to the new roles + new_assignments.each do |assignment| + if assignment['new_role_id'] + ActiveRecord::Base.connection.execute("INSERT INTO users_roles (user_id, role_id)" \ + " VALUES (#{assignment['user_id']}, #{assignment['new_role_id']})") + end + end + end + + def down + drop_table :roles + + create_table(:roles) do |t| + t.string :name + t.references :resource, polymorphic: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e6fce2213c..874ebad819 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,53 +10,59 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190711192033) do +ActiveRecord::Schema.define(version: 2019_07_26_153012) do create_table "features", force: :cascade do |t| - t.integer "setting_id" - t.string "name", null: false - t.string "value" - t.boolean "enabled", default: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "setting_id" + t.string "name", null: false + t.string "value" + t.boolean "enabled", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["name"], name: "index_features_on_name" t.index ["setting_id"], name: "index_features_on_setting_id" end create_table "invitations", force: :cascade do |t| - t.string "email", null: false - t.string "provider", null: false - t.string "invite_token" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "email", null: false + t.string "provider", null: false + t.string "invite_token" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["invite_token"], name: "index_invitations_on_invite_token" t.index ["provider"], name: "index_invitations_on_provider" end create_table "roles", force: :cascade do |t| - t.string "name" - t.string "resource_type" - t.integer "resource_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" + t.string "name" + t.integer "priority", default: 9999 + t.boolean "can_create_rooms", default: false + t.boolean "send_promoted_email", default: false + t.boolean "send_demoted_email", default: false + t.boolean "can_edit_site_settings", default: false + t.boolean "can_edit_roles", default: false + t.boolean "can_manage_users", default: false + t.string "colour" + t.string "provider" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name", "provider"], name: "index_roles_on_name_and_provider", unique: true t.index ["name"], name: "index_roles_on_name" - t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id" end create_table "rooms", force: :cascade do |t| - t.integer "user_id" - t.string "name" - t.string "uid" - t.string "bbb_id" - t.integer "sessions", default: 0 + t.integer "user_id" + t.string "name" + t.string "uid" + t.string "bbb_id" + t.integer "sessions", default: 0 t.datetime "last_session" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "room_settings", default: "{ }" - t.string "moderator_pw" - t.string "attendee_pw" - t.string "access_code" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "room_settings", default: "{ }" + t.string "moderator_pw" + t.string "attendee_pw" + t.string "access_code" t.index ["bbb_id"], name: "index_rooms_on_bbb_id" t.index ["last_session"], name: "index_rooms_on_last_session" t.index ["name"], name: "index_rooms_on_name" @@ -66,30 +72,30 @@ end create_table "settings", force: :cascade do |t| - t.string "provider", null: false + t.string "provider", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["provider"], name: "index_settings_on_provider" end create_table "users", force: :cascade do |t| - t.integer "room_id" - t.string "provider" - t.string "uid" - t.string "name" - t.string "username" - t.string "email" - t.string "social_uid" - t.string "image" - t.string "password_digest" - t.boolean "accepted_terms", default: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "email_verified", default: false - t.string "language", default: "default" - t.string "reset_digest" + t.integer "room_id" + t.string "provider" + t.string "uid" + t.string "name" + t.string "username" + t.string "email" + t.string "social_uid" + t.string "image" + t.string "password_digest" + t.boolean "accepted_terms", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "email_verified", default: false + t.string "language", default: "default" + t.string "reset_digest" t.datetime "reset_sent_at" - t.string "activation_digest" + t.string "activation_digest" t.datetime "activated_at" t.index ["created_at"], name: "index_users_on_created_at" t.index ["email"], name: "index_users_on_email" diff --git a/db/seeds.rb b/db/seeds.rb index 8239e709f7..cfcacc56b8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -8,4 +8,5 @@ # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) +Role.create_default_roles("greenlight") Rake::Task['admin:create'].invoke diff --git a/spec/controllers/admins_controller_spec.rb b/spec/controllers/admins_controller_spec.rb index ffa228f19a..f76bf33f75 100644 --- a/spec/controllers/admins_controller_spec.rb +++ b/spec/controllers/admins_controller_spec.rb @@ -22,6 +22,7 @@ before do allow_any_instance_of(ApplicationController).to receive(:set_user_domain).and_return("provider1") controller.instance_variable_set(:@user_domain, "provider1") + @user = create(:user, provider: "provider1") @admin = create(:user, provider: "provider1") @admin.add_role :admin @@ -58,53 +59,6 @@ end end - context "POST #promote" do - it "promotes a user to admin" do - @request.session[:user_id] = @admin.id - - expect(@user.has_role?(:admin)).to eq(false) - - post :promote, params: { user_uid: @user.uid } - - expect(@user.has_role?(:admin)).to eq(true) - expect(flash[:success]).to be_present - expect(response).to redirect_to(admins_path) - end - - it "sends an email to the user being promoted" do - @request.session[:user_id] = @admin.id - - params = { user_uid: @user.uid } - - expect { post :promote, params: params }.to change { ActionMailer::Base.deliveries.count }.by(1) - end - end - - context "POST #demote" do - it "demotes an admin to user" do - @request.session[:user_id] = @admin.id - - @user.add_role :admin - expect(@user.has_role?(:admin)).to eq(true) - - post :demote, params: { user_uid: @user.uid } - - expect(@user.has_role?(:admin)).to eq(false) - expect(flash[:success]).to be_present - expect(response).to redirect_to(admins_path) - end - - it "sends an email to the user being demoted" do - @request.session[:user_id] = @admin.id - - @user.add_role :admin - - params = { user_uid: @user.uid } - - expect { post :demote, params: params }.to change { ActionMailer::Base.deliveries.count }.by(1) - end - end - context "POST #ban" do it "bans a user from the application" do @request.session[:user_id] = @admin.id @@ -331,4 +285,239 @@ end end end + + describe "Roles" do + context "GET #roles" do + it "should render the roles editor with the user role selected" do + @request.session[:user_id] = @admin.id + + get :roles + + expect(response).to render_template :roles + expect(assigns(:roles).count).to eq(2) + expect(assigns(:selected_role).name).to eq("user") + end + + it "should render the roles editor with the request role selected" do + Role.create_default_roles("provider1") + + new_role = Role.create(name: "test", provider: "provider1") + + @request.session[:user_id] = @admin.id + + get :roles, params: { selected_role: new_role.id } + + expect(response).to render_template :roles + expect(assigns(:roles).count).to eq(3) + expect(assigns(:selected_role).name).to eq(new_role.name) + end + end + + context "POST #new_role" do + before do + Role.create_default_roles("provider1") + end + + it "should fail with duplicate role name" do + @request.session[:user_id] = @admin.id + + post :new_role, params: { role: { name: "admin" } } + + expect(response).to redirect_to admin_roles_path + expect(flash[:alert]).to eq(I18n.t("administrator.roles.duplicate_name")) + end + + it "should fail with empty role name" do + @request.session[:user_id] = @admin.id + + post :new_role, params: { role: { name: " " } } + + expect(response).to redirect_to admin_roles_path + expect(flash[:alert]).to eq(I18n.t("administrator.roles.empty_name")) + end + + it "should create new role and increase user role priority" do + @request.session[:user_id] = @admin.id + + post :new_role, params: { role: { name: "test" } } + + new_role = Role.find_by(name: "test", provider: "provider1") + user_role = Role.find_by(name: "user", provider: "provider1") + + expect(new_role.priority).to eq(1) + expect(user_role.priority).to eq(2) + expect(response).to redirect_to admin_roles_path(selected_role: new_role.id) + end + end + + context "PATCH #change_role_order" do + before do + Role.create_default_roles("provider1") + end + + it "should fail if user attempts to change the order of the admin or user roles" do + @request.session[:user_id] = @admin.id + + user_role = Role.find_by(name: "user", provider: "provider1") + admin_role = Role.find_by(name: "admin", provider: "provider1") + + patch :change_role_order, params: { role: [user_role.id, admin_role.id] } + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.invalid_order")) + expect(response).to redirect_to admin_roles_path + end + + it "should fail if a user attempts to edit a role with a higher priority than their own" do + Role.create(name: "test1", priority: 1, provider: "greenlight") + new_role2 = Role.create(name: "test2", priority: 2, provider: "greenlight", can_edit_roles: true) + new_role3 = Role.create(name: "test3", priority: 3, provider: "greenlight") + user_role = Role.find_by(name: "user", provider: "greenlight") + + user_role.priority = 4 + user_role.save! + + @user.roles << new_role2 + @user.save! + + @request.session[:user_id] = @user.id + + patch :change_role_order, params: { role: [new_role3.id, new_role2.id] } + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.invalid_update")) + expect(response).to redirect_to admin_roles_path + end + + it "should fail if a user attempts to edit a role with a higher priority than their own" do + Role.create(name: "test1", priority: 1, provider: "greenlight") + new_role2 = Role.create(name: "test2", priority: 2, provider: "greenlight", can_edit_roles: true) + new_role3 = Role.create(name: "test3", priority: 3, provider: "greenlight") + user_role = Role.find_by(name: "user", provider: "greenlight") + + user_role.priority = 4 + user_role.save! + + @user.roles << new_role2 + @user.save! + + @request.session[:user_id] = @user.id + + patch :change_role_order, params: { role: [new_role3.id, new_role2.id] } + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.invalid_update")) + expect(response).to redirect_to admin_roles_path + end + + it "should update the role order" do + new_role1 = Role.create(name: "test1", priority: 1, provider: "provider1") + new_role2 = Role.create(name: "test2", priority: 2, provider: "provider1") + new_role3 = Role.create(name: "test3", priority: 3, provider: "provider1") + user_role = Role.find_by(name: "user", provider: "provider1") + + @request.session[:user_id] = @admin.id + + patch :change_role_order, params: { role: [new_role3.id, new_role2.id, new_role1.id] } + + new_role1.reload + new_role2.reload + new_role3.reload + user_role.reload + + expect(new_role3.priority).to eq(1) + expect(new_role2.priority).to eq(2) + expect(new_role1.priority).to eq(3) + expect(user_role.priority).to eq(4) + end + end + + context 'POST #update_role' do + before do + Role.create_default_roles("provider1") + end + + it "should fail to update a role with a lower priority than the user" do + new_role1 = Role.create(name: "test1", priority: 1, provider: "provider1") + new_role2 = Role.create(name: "test2", priority: 2, provider: "provider1", can_edit_roles: true) + user_role = Role.find_by(name: "user", provider: "greenlight") + + user_role.priority = 3 + user_role.save! + + @user.roles << new_role2 + @user.save! + + @request.session[:user_id] = @user.id + + patch :update_role, params: { role_id: new_role1.id } + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.invalid_update")) + expect(response).to redirect_to admin_roles_path(selected_role: new_role1.id) + end + + it "should fail to update if there is a duplicate name" do + new_role = Role.create(name: "test2", priority: 1, provider: "provider1", can_edit_roles: true) + + @request.session[:user_id] = @admin.id + + patch :update_role, params: { role_id: new_role.id, role: { name: "admin" } } + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.duplicate_name")) + expect(response).to redirect_to admin_roles_path(selected_role: new_role.id) + end + + it "should update role permisions" do + new_role = Role.create(name: "test2", priority: 1, provider: "provider1", can_edit_roles: true) + + @request.session[:user_id] = @admin.id + + patch :update_role, params: { role_id: new_role.id, role: { name: "test", can_edit_roles: false, + colour: "#45434", can_manage_users: true } } + + new_role.reload + expect(new_role.name).to eq("test") + expect(new_role.can_edit_roles).to eq(false) + expect(new_role.colour).to eq("#45434") + expect(new_role.can_manage_users).to eq(true) + expect(new_role.send_promoted_email).to eq(false) + expect(response).to redirect_to admin_roles_path(selected_role: new_role.id) + end + end + + context "DELETE delete_role" do + before do + Role.create_default_roles("provider1") + end + + it "should fail to delete the role if it has users assigned to it" do + admin_role = Role.find_by(name: "admin", provider: "greenlight") + + @request.session[:user_id] = @admin.id + + delete :delete_role, params: { role_id: admin_role.id } + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.role_has_users", user_count: 1)) + expect(response).to redirect_to admin_roles_path(selected_role: admin_role.id) + end + + it "should fail to delete the role if it is a default role" do + pending_role = Role.find_by(name: "pending", provider: "provider1") + + @request.session[:user_id] = @admin.id + + delete :delete_role, params: { role_id: pending_role.id } + + expect(response).to redirect_to admin_roles_path(selected_role: pending_role.id) + end + + it "should successfully delete the role" do + new_role = Role.create(name: "test2", priority: 1, provider: "provider1", can_edit_roles: true) + + @request.session[:user_id] = @admin.id + + delete :delete_role, params: { role_id: new_role.id } + + expect(Role.where(name: "test2", provider: "provider1").count).to eq(0) + expect(response).to redirect_to admin_roles_path + end + end + end end diff --git a/spec/controllers/rooms_controller_spec.rb b/spec/controllers/rooms_controller_spec.rb index 9e64af68e8..001b82c275 100644 --- a/spec/controllers/rooms_controller_spec.rb +++ b/spec/controllers/rooms_controller_spec.rb @@ -61,6 +61,19 @@ def random_valid_room_params expect(response).to render_template(:join) end + it "should render cant_create_rooms if user doesn't have permission to create rooms" do + user_role = @user.highest_priority_role + + user_role.can_create_rooms = false + user_role.save! + + @request.session[:user_id] = @user.id + + get :show, params: { room_uid: @user.main_room } + + expect(response).to render_template(:cant_create_rooms) + end + it "should be able to search public recordings if user is not owner" do @request.session[:user_id] = @user.id @@ -454,4 +467,32 @@ def random_valid_room_params expect(flash[:alert]).to eq(I18n.t("room.access_code_required")) end end + + describe "POST join_specific_room" do + before do + @user = create(:user) + @user1 = create(:user) + end + + it "should display flash if the user doesn't supply a valid uid" do + @request.session[:user_id] = @user.id + + post :join_specific_room, params: { join_room: { url: "abc" } } + + expect(flash[:alert]).to eq(I18n.t("room.no_room.invalid_room_uid")) + expect(response).to redirect_to room_path(@user.main_room) + end + + it "should redirect the user to the room uid they supplied" do + post :join_specific_room, params: { join_room: { url: @user1.main_room } } + + expect(response).to redirect_to room_path(@user1.main_room) + end + + it "should redirect the user to the room join url they supplied" do + post :join_specific_room, params: { join_room: { url: room_path(@user1.main_room) } } + + expect(response).to redirect_to room_path(@user1.main_room) + end + end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index b0af1bfad7..5af84f782c 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -333,6 +333,95 @@ def random_valid_user_params patch :update, params: invalid_params.merge!(user_uid: @user) expect(response).to render_template(:edit) end + + context 'Roles updates' do + it "should fail to update roles if users tries to add a role with a higher priority than their own" do + user = create(:user) + @request.session[:user_id] = user.id + + user_role = user.highest_priority_role + + user_role.can_edit_roles = true + + user_role.save! + + tmp_role = Role.create(name: "test", priority: -2, provider: "greenlight") + + params = random_valid_user_params + patch :update, params: params.merge!(user_uid: user, user: { role_ids: tmp_role.id.to_s }) + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.invalid_assignment")) + expect(response).to render_template(:edit) + end + + it "should fail to update roles if a user tries to remove a role with a higher priority than their own" do + user = create(:user) + admin = create(:user) + + admin.add_role :admin + + @request.session[:user_id] = user.id + + user_role = user.highest_priority_role + + user_role.can_edit_roles = true + + user_role.save! + + params = random_valid_user_params + patch :update, params: params.merge!(user_uid: admin, user: { role_ids: "" }) + + user.reload + + expect(flash[:alert]).to eq(I18n.t("administrator.roles.invalid_removal")) + expect(response).to render_template(:edit) + end + + it "should successfuly add roles to the user" do + allow(Rails.configuration).to receive(:enable_email_verification).and_return(true) + + user = create(:user) + admin = create(:user) + + admin.add_role :admin + + @request.session[:user_id] = admin.id + + tmp_role1 = Role.create(name: "test1", priority: 1, provider: "greenlight", send_promoted_email: true) + tmp_role2 = Role.create(name: "test2", priority: 2, provider: "greenlight") + + params = random_valid_user_params + params = params.merge!(user_uid: user, user: { role_ids: "#{tmp_role1.id} #{tmp_role2.id}" }) + + expect { patch :update, params: params }.to change { ActionMailer::Base.deliveries.count }.by(1) + expect(user.roles.count).to eq(2) + expect(user.highest_priority_role.name).to eq("test1") + expect(response).to redirect_to(admins_path) + end + + it "all users must at least have the user role" do + allow(Rails.configuration).to receive(:enable_email_verification).and_return(true) + + user = create(:user) + admin = create(:user) + + admin.add_role :admin + + tmp_role1 = Role.create(name: "test1", priority: 1, provider: "greenlight", send_demoted_email: true) + user.roles << tmp_role1 + user.save! + + @request.session[:user_id] = admin.id + + params = random_valid_user_params + params = params.merge!(user_uid: user, user: { role_ids: "" }) + + expect { patch :update, params: params }.to change { ActionMailer::Base.deliveries.count }.by(1) + expect(user.roles.count).to eq(1) + expect(user.highest_priority_role.name).to eq("user") + expect(response).to redirect_to(admins_path) + end + end end describe "DELETE #user" do diff --git a/spec/helpers/admins_helper_spec.rb b/spec/helpers/admins_helper_spec.rb new file mode 100644 index 0000000000..e690bd35ef --- /dev/null +++ b/spec/helpers/admins_helper_spec.rb @@ -0,0 +1,44 @@ +# 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 . + +require "rails_helper" + +describe AdminsHelper do + describe "edit_disabled" do + it "should disable inputs for roles with a higher priority" do + user = create(:user) + admin_role = Role.find_by(name: "admin", provider: "greenlight") + helper.instance_variable_set(:@selected_role, admin_role) + + allow_any_instance_of(SessionsHelper).to receive(:current_user).and_return(user) + + expect(helper.edit_disabled).to eq(true) + end + + it "should enable inputs for roles with a lower priority" do + user = create(:user) + user.roles << Role.find_by(name: "admin", provider: "greenlight") + user_role = Role.find_by(name: "user", provider: "greenlight") + helper.instance_variable_set(:@selected_role, user_role) + + allow_any_instance_of(SessionsHelper).to receive(:current_user).and_return(user) + + expect(helper.edit_disabled).to eq(false) + end + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 24c8cd0d11..d3514529e5 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -85,4 +85,14 @@ expect(helper.allow_greenlight_accounts?).to eql(false) end end + + describe "role_clour" do + it "should use default if the user doens't have a role" do + expect(helper.role_colour(Role.create(name: "test"))).to eq(Rails.configuration.primary_color_default) + end + + it "should use role colour if provided" do + expect(helper.role_colour(Role.create(name: "test", colour: "#1234"))).to eq("#1234") + end + end end diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb new file mode 100644 index 0000000000..eabe2661d0 --- /dev/null +++ b/spec/helpers/users_helper_spec.rb @@ -0,0 +1,44 @@ +# 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 . + +require "rails_helper" + +describe UsersHelper do + describe "disabled roles" do + it "should return roles with a less than or equal to priority for non admins" do + user = create(:user) + allow_any_instance_of(SessionsHelper).to receive(:current_user).and_return(user) + + disabled_roles = helper.disabled_roles(user) + + expect(disabled_roles.count).to eq(1) + end + + it "should return roles with a lesser priority for admins" do + admin = create(:user) + admin.add_role :admin + user = create(:user) + + allow_any_instance_of(SessionsHelper).to receive(:current_user).and_return(admin) + + disabled_roles = helper.disabled_roles(user) + + expect(disabled_roles.count).to eq(1) + end + end +end diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb new file mode 100644 index 0000000000..c8a299a62a --- /dev/null +++ b/spec/models/role_spec.rb @@ -0,0 +1,35 @@ +# 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 . + +require "rails_helper" + +describe Role, type: :model do + it "should return duplicate if role name is in reserved role names" do + expect(Role.duplicate_name("admin", "greenlight")).to eq(true) + end + + it "should return duplicate if role name matched another" do + Role.create(name: "test", provider: "greenlight") + expect(Role.duplicate_name("test", "greenlight")).to eq(true) + end + + it "should return false role name doesn't exist" do + Role.create(name: "test", provider: "greenlight") + expect(Role.duplicate_name("test1", "greenlight")).to eq(false) + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fefb0b20c2..44568837d3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -158,6 +158,72 @@ expect(@admin.admin_of?(@user)).to be false end + + it "should get the highest priority role" do + @admin = create(:user, provider: @user.provider) + @admin.add_role :admin + + expect(@admin.highest_priority_role.name).to eq("admin") + end + + it "should skip adding the role if the user already has the role" do + @admin = create(:user, provider: @user.provider) + @admin.add_role :admin + @admin.add_role :admin + + expect(@admin.roles.count).to eq(2) + end + + it "should add the role if the user doesn't already have the role" do + @admin = create(:user, provider: @user.provider) + @admin.add_role :admin + + expect(@admin.roles.count).to eq(2) + end + + it "should remove the role if the user has the role assigned to them" do + @admin = create(:user, provider: @user.provider) + @admin.add_role :admin + @admin.remove_role :admin + + expect(@admin.roles.count).to eq(1) + end + + it "has_role? should return false if the user doesn't have the role" do + expect(@user.has_role?(:admin)).to eq(false) + end + + it "has_role? should return true if the user has the role" do + @admin = create(:user, provider: @user.provider) + @admin.add_role :admin + + expect(@admin.has_role?(:admin)).to eq(true) + end + + it "with_role should return all users with the role" do + @admin1 = create(:user, provider: @user.provider) + @admin2 = create(:user, provider: @user.provider) + @admin1.add_role :admin + @admin2.add_role :admin + + expect(User.with_role(:admin).count).to eq(2) + end + + it "without_role should return all users without the role" do + @admin1 = create(:user, provider: @user.provider) + @admin2 = create(:user, provider: @user.provider) + @admin1.add_role :admin + @admin2.add_role :admin + + expect(User.without_role(:admin).count).to eq(1) + end + + it "all_users_with_roles should return all users with at least one role" do + @admin1 = create(:user, provider: @user.provider) + @admin2 = create(:user, provider: @user.provider) + + expect(User.all_users_with_roles.count).to eq(3) + end end context 'blank email' do diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb index 7254af5095..00e6fcae7b 100644 --- a/test/mailers/previews/user_mailer_preview.rb +++ b/test/mailers/previews/user_mailer_preview.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class UserMailerPreview < ActionMailer::Preview - def initialize + def initialize(_params) @logo = "https://raw.githubusercontent.com/bigbluebutton/greenlight/master/app/assets/images/logo_with_text.png" @color = "#467fcf" end @@ -53,19 +53,21 @@ def invite_user_signup # http://localhost:3000/rails/mailers/user_mailer/user_promoted def user_promoted user = User.first + role = Role.first.name url = "http://example.com" logo_image = "https://raw.githubusercontent.com/bigbluebutton/greenlight/master/app/assets/images/logo_with_text.png" user_color = "#467fcf" - UserMailer.user_promoted(user, url, logo_image, user_color) + UserMailer.user_promoted(user, role, url, logo_image, user_color) end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/user_demoted def user_demoted user = User.first + role = Role.first.name url = "http://example.com" logo_image = "https://raw.githubusercontent.com/bigbluebutton/greenlight/master/app/assets/images/logo_with_text.png" user_color = "#467fcf" - UserMailer.user_demoted(user, url, logo_image, user_color) + UserMailer.user_demoted(user, role, url, logo_image, user_color) end end diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +