From 59e87b22fe4123f4f8df2d89ef8407e0b2b12e0f Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Thu, 12 Dec 2024 15:39:33 +0100 Subject: [PATCH] Add permissions and permission controllers for forms [SCI-11355] --- .../access_permissions/forms_controller.rb | 186 ++++++++++++++++++ app/controllers/form_fields_controller.rb | 15 +- app/controllers/forms_controller.rb | 31 ++- app/javascript/vue/forms/table.vue | 29 ++- app/models/form.rb | 45 +++++ app/permissions/form.rb | 41 ++++ app/permissions/team.rb | 4 + app/serializers/form_serializer.rb | 2 +- app/serializers/lists/form_serializer.rb | 45 ++++- app/services/lists/forms_service.rb | 2 +- app/services/toolbars/forms_service.rb | 16 +- .../forms/edit.json.jbuilder | 16 ++ .../forms/form_member.json.jbuilder | 13 ++ .../forms/new.json.jbuilder | 14 ++ .../forms/show.json.jbuilder | 12 ++ app/views/forms/index.html.erb | 1 + .../references/_form.html.erb | 15 ++ config/initializers/extends.rb | 15 +- .../extends/permission_extends.rb | 17 ++ config/locales/en.yml | 9 + config/locales/global_activities/en.yml | 8 + config/routes.rb | 5 + .../20241211100730_add_form_permissions.rb | 47 +++++ ..._add_public_visibility_columns_to_forms.rb | 10 + .../form_fields_controller_spec.rb | 10 +- spec/controllers/forms_controller_spec.rb | 36 +--- spec/models/form_field_spec.rb | 1 - 27 files changed, 587 insertions(+), 58 deletions(-) create mode 100644 app/controllers/access_permissions/forms_controller.rb create mode 100644 app/permissions/form.rb create mode 100644 app/views/access_permissions/forms/edit.json.jbuilder create mode 100644 app/views/access_permissions/forms/form_member.json.jbuilder create mode 100644 app/views/access_permissions/forms/new.json.jbuilder create mode 100644 app/views/access_permissions/forms/show.json.jbuilder create mode 100644 app/views/global_activities/references/_form.html.erb create mode 100644 db/migrate/20241211100730_add_form_permissions.rb create mode 100644 db/migrate/20241211105449_add_public_visibility_columns_to_forms.rb diff --git a/app/controllers/access_permissions/forms_controller.rb b/app/controllers/access_permissions/forms_controller.rb new file mode 100644 index 000000000..e719aeef9 --- /dev/null +++ b/app/controllers/access_permissions/forms_controller.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +module AccessPermissions + class FormsController < ApplicationController + include InputSanitizeHelper + + before_action :set_form + before_action :check_read_permissions, only: %i(show) + before_action :check_manage_permissions, except: %i(show) + before_action :available_users, only: %i(new create) + + def show + render json: @form.user_assignments.includes(:user_role, :user).order('users.full_name ASC'), + each_serializer: UserAssignmentSerializer, user: current_user + end + + def new + render json: @available_users, each_serializer: UserSerializer, user: current_user + end + + def edit; end + + def create + ActiveRecord::Base.transaction do + created_count = 0 + if permitted_create_params[:user_id] == 'all' + @form.update!(visibility: :visible, default_public_user_role_id: permitted_create_params[:user_role_id]) + log_activity(:form_access_granted_all_team_members, + { team: @form.team.id, role: @form.default_public_user_role&.name }) + else + user_assignment = UserAssignment.find_or_initialize_by( + assignable: @form, + user_id: permitted_create_params[:user_id], + team: current_team + ) + + user_assignment.update!( + user_role_id: permitted_create_params[:user_role_id], + assigned_by: current_user, + assigned: :manually + ) + + log_activity(:form_access_granted, { user_target: user_assignment.user.id, + role: user_assignment.user_role.name }) + created_count += 1 + end + + @message = if created_count.zero? + t('access_permissions.create.success', member_name: t('access_permissions.all_team')) + else + t('access_permissions.create.success', member_name: escape_input(user_assignment.user.name)) + end + render json: { message: @message } + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error e.message + errors = @form.errors.present? ? @form.errors&.map(&:message)&.join(',') : e.message + render json: { flash: errors }, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + end + + def update + @user_assignment = @form.user_assignments.find_by( + user_id: permitted_update_params[:user_id], + team: current_team + ) + + # prevent role change if it would result in no manually assigned users having the user management permission + new_user_role = UserRole.find(permitted_update_params[:user_role_id]) + if !new_user_role.has_permission?(FormPermissions::USERS_MANAGE) && + @user_assignment.last_with_permission?(FormPermissions::USERS_MANAGE, assigned: :manually) + raise ActiveRecord::RecordInvalid + end + + @user_assignment.update!(permitted_update_params) + log_activity(:form_access_changed, { user_target: @user_assignment.user.id, + role: @user_assignment.user_role.name }) + + render :form_member + rescue ActiveRecord::RecordInvalid + render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity + end + + def destroy + user = @form.assigned_users.find(params[:user_id]) + user_assignment = @form.user_assignments.find_by(user: user, team: current_team) + + # prevent deletion of last manually assigned user that can manage users + raise ActiveRecord::RecordInvalid if user_assignment.last_with_permission?(FormPermissions::USERS_MANAGE, assigned: :manually) + + Protocol.transaction do + if @form.visible? + user_assignment.update!( + user_role: @form.default_public_user_role, + assigned: :automatically + ) + else + user_assignment.destroy! + end + log_activity(:form_access_revoked, { user_target: user_assignment.user.id, + role: user_assignment.user_role.name }) + end + + render json: { message: t('access_permissions.destroy.success', member_name: user.full_name) } + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error e.message + render json: { message: t('access_permissions.destroy.failure') }, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + + def update_default_public_user_role + ActiveRecord::Base.transaction do + current_role = @form.default_public_user_role.name + @form.update!(permitted_default_public_user_role_params) + + # revoke all team members access + if permitted_default_public_user_role_params[:default_public_user_role_id].blank? + log_activity(:form_access_revoked_all_team_members, + { team: @form.team.id, role: current_role }) + render json: { flash: t('access_permissions.update.revoke_all_team_members') }, status: :ok + else + # update all team members access + log_activity(:form_access_changed_all_team_members, + { team: @form.team.id, role: @form.default_public_user_role&.name }) + end + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error e.message + render json: { flash: @form.errors&.map(&:message)&.join(',') }, status: :unprocessable_entity + raise ActiveRecord::Rollback + end + end + + private + + def permitted_default_public_user_role_params + params.require(:object).permit(:default_public_user_role_id) + end + + def permitted_update_params + params.require(:user_assignment) + .permit(%i(user_role_id user_id)) + end + + def permitted_create_params + params.require(:user_assignment) + .permit(%i(user_id user_role_id)) + end + + def available_users + # automatically assigned or not assigned to project + @available_users = current_team.users.where( + id: @form.user_assignments.automatically_assigned.select(:user_id) + ).or( + current_team.users.where.not(id: @form.users.select(:id)) + ).order('users.full_name ASC') + end + + def set_form + @form = current_team.forms.includes(user_assignments: %i(user user_role)).find_by(id: params[:id]) + + return render_404 unless @form + + @form = @form.parent if @form.parent_id + end + + def check_manage_permissions + render_403 unless can_manage_form_users?(@form) + end + + def check_read_permissions + render_403 unless can_read_form?(@form) + end + + def log_activity(type_of, message_items = {}) + message_items = { form: @form.id }.merge(message_items) + + Activities::CreateActivityService + .call(activity_type: type_of, + owner: current_user, + subject: @form, + team: @form.team, + project: nil, + message_items: message_items) + end + end +end diff --git a/app/controllers/form_fields_controller.rb b/app/controllers/form_fields_controller.rb index 626ced25d..5e272d8f0 100644 --- a/app/controllers/form_fields_controller.rb +++ b/app/controllers/form_fields_controller.rb @@ -3,6 +3,7 @@ class FormFieldsController < ApplicationController before_action :load_form before_action :load_form_field, only: %i(update destroy) + before_action :check_manage_permissions, only: %i(create update destroy reorder) def create ActiveRecord::Base.transaction do @@ -47,14 +48,14 @@ class FormFieldsController < ApplicationController def reorder ActiveRecord::Base.transaction do params.permit(form_field_positions: %i(position id))[:form_field_positions].each do |data| - form_field = @form.form_fields.find(data[:id]) - form_field.insert_at(data[:position].to_i) + @form_field = @form.form_fields.find(data[:id]) + @form_field.insert_at!(data[:position].to_i) end end render json: params[:form_field_positions], status: :ok rescue ActiveRecord::RecordInvalid - render json: { errors: form_field.errors }, status: :conflict + render json: { errors: @form_field.errors }, status: :unprocessable_entity end private @@ -62,13 +63,17 @@ class FormFieldsController < ApplicationController def load_form @form = Form.find_by(id: params[:form_id]) - return render_404 unless @form + render_404 unless @form end def load_form_field @form_field = @form.form_fields.find_by(id: params[:id]) - return render_404 unless @form_field + render_404 unless @form_field + end + + def check_manage_permissions + render_403 unless @form && can_manage_form?(@form) end def form_field_params diff --git a/app/controllers/forms_controller.rb b/app/controllers/forms_controller.rb index 577d52ca2..ef16c8544 100644 --- a/app/controllers/forms_controller.rb +++ b/app/controllers/forms_controller.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class FormsController < ApplicationController + include UserRolesHelper + before_action :load_form, only: %i(show update publish unpublish) before_action :set_breadcrumbs_items, only: %i(index show) + before_action :check_manage_permissions, only: %i(update publish unpublish) + before_action :check_create_permissions, only: :create def index respond_to do |format| @@ -34,7 +38,7 @@ class FormsController < ApplicationController ) if @form.save - render json: @form, serializer: Lists::FormSerializer, user: current_user + render json: @form, serializer: FormSerializer, user: current_user else render json: { error: @form.errors.full_messages }, status: :unprocessable_entity end @@ -44,7 +48,7 @@ class FormsController < ApplicationController def update ActiveRecord::Base.transaction do if @form.update(form_params.merge({ last_modified_by: current_user })) - render json: @form, serializer: Lists::FormSerializer, user: current_user + render json: @form, serializer: FormSerializer, user: current_user else render json: { error: @form.errors.full_messages }, status: :unprocessable_entity end @@ -58,7 +62,7 @@ class FormsController < ApplicationController published_on: DateTime.now ) - render json: @form, serializer: Lists::FormSerializer, user: current_user + render json: @form, serializer: FormSerializer, user: current_user end end @@ -69,13 +73,14 @@ class FormsController < ApplicationController published_on: nil ) - render json: @form, serializer: Lists::FormSerializer, user: current_user + render json: @form, serializer: FormSerializer, user: current_user end end def archive forms = current_team.forms.active.where(id: params[:form_ids]) return render_404 if forms.blank? + return render_403 unless forms.all? { |f| can_archive_form?(f) } counter = 0 @@ -99,6 +104,7 @@ class FormsController < ApplicationController def restore forms = current_team.forms.archived.where(id: params[:form_ids]) return render_404 if forms.blank? + return render_403 unless forms.all? { |f| can_restore_form?(f) } counter = 0 @@ -129,6 +135,10 @@ class FormsController < ApplicationController } end + def user_roles + render json: { data: user_roles_collection(Form.new).map(&:reverse) } + end + private def set_breadcrumbs_items @@ -150,9 +160,18 @@ class FormsController < ApplicationController end def load_form - @form = Form.find_by(id: params[:id]) + @form = current_team.forms.readable_by_user(current_user).find_by(id: params[:id]) - return render_404 unless @form + render_404 unless @form + end + + def check_create_permissions + render_403 unless can_create_forms?(current_team) + end + + def check_manage_permissions + + render_403 unless @form && can_manage_form?(@form) end def form_params diff --git a/app/javascript/vue/forms/table.vue b/app/javascript/vue/forms/table.vue index c5694de0f..89da5eaa7 100644 --- a/app/javascript/vue/forms/table.vue +++ b/app/javascript/vue/forms/table.vue @@ -8,8 +8,11 @@ :actionsUrl="actionsUrl" @tableReloaded="reloadingTable = false" @create="createForm" + @access="access" /> +