Merge pull request #8663 from artoscinote/ma_SCI_12055

Refactor sharing logic [SCI-12055]
This commit is contained in:
Martin Artnik 2025-07-18 09:55:38 +02:00 committed by GitHub
commit 52c72d2b20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 87 additions and 206 deletions

View file

@ -3,6 +3,7 @@
module AccessPermissions
class BaseController < ApplicationController
include InputSanitizeHelper
include UserRolesHelper
before_action :set_model
before_action :set_assignment, only: %i(create update destroy)
@ -11,7 +12,7 @@ module AccessPermissions
before_action :load_available_users, only: %i(new create)
def show
render json: @model.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
render json: @model.user_assignments.where(team: current_team).includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer, user: current_user
end
@ -114,6 +115,10 @@ module AccessPermissions
each_serializer: UserGroupSerializer, user: current_user
end
def user_roles
render json: { data: user_roles_collection(@model).map(&:reverse) }
end
private
def model_parameter

View file

@ -5,7 +5,7 @@ module AccessPermissions
private
def set_model
@model = current_team.repositories.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
@model = Repository.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @model
end

View file

@ -2,7 +2,6 @@
class FormsController < ApplicationController
include InputSanitizeHelper
include UserRolesHelper
before_action :check_forms_enabled
before_action :load_form, only: %i(show update publish unpublish export_form_responses duplicate)
@ -210,10 +209,6 @@ class FormsController < ApplicationController
}
end
def user_roles
render json: { data: user_roles_collection(Form.new).map(&:reverse) }
end
private
def set_breadcrumbs_items

View file

@ -8,7 +8,6 @@ class ProjectsController < ApplicationController
include CardsViewHelper
include ExperimentsHelper
include Breadcrumbs
include UserRolesHelper
include FavoritesActions
attr_reader :current_folder
@ -20,7 +19,7 @@ class ProjectsController < ApplicationController
before_action :load_current_folder, only: :index
before_action :check_read_permissions, except: %i(index create update archive_group restore_group
inventory_assigning_project_filter
actions_toolbar user_roles users_filter head_of_project_users_list
actions_toolbar users_filter head_of_project_users_list
favorite unfavorite)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: :update
@ -300,10 +299,6 @@ class ProjectsController < ApplicationController
render json: { data: users.map { |u| [u.id, u.name, { avatar_url: avatar_path(u, :icon_small) }] } }, status: :ok
end
def user_roles
render json: { data: user_roles_collection(Project.new).map(&:reverse) }
end
def actions_toolbar
render json: {
actions:

View file

@ -7,7 +7,6 @@ class ProtocolsController < ApplicationController
include ProtocolsIoHelper
include TeamsHelper
include ProtocolsExporterV2
include UserRolesHelper
include FormFieldValuesHelper
before_action :check_create_permissions, only: %i(
@ -877,10 +876,6 @@ class ProtocolsController < ApplicationController
render json: { job_id: @job.job_id }
end
def user_roles
render json: { data: user_roles_collection(Protocol.new).map(&:reverse) }
end
private
def set_importer

View file

@ -8,18 +8,17 @@ class RepositoriesController < ApplicationController
include TeamsHelper
include RepositoriesDatatableHelper
include MyModulesHelper
include UserRolesHelper
before_action :switch_team_with_param, only: %i(index)
before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar
export_repositories list user_roles)
export_repositories list)
before_action :load_repositories, only: %i(index list)
before_action :load_repositories_for_archiving, only: :archive
before_action :load_repositories_for_restoring, only: :restore
before_action :check_view_all_permissions, only: %i(index sidebar list)
before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet
import_records sidebar archive restore actions_toolbar
export_repositories list user_roles)
export_repositories list)
before_action :check_manage_permissions, only: %i(rename_modal update)
before_action :check_delete_permissions, only: %i(destroy destroy_modal)
before_action :check_archive_permissions, only: %i(archive restore)
@ -483,10 +482,6 @@ class RepositoriesController < ApplicationController
}
end
def user_roles
render json: { data: user_roles_collection(Repository.new).map(&:reverse) }
end
private
def load_repository

View file

@ -88,7 +88,7 @@ class TeamSharedObjectsController < ApplicationController
def check_sharing_permissions
object_name = @model.is_a?(RepositoryBase) ? 'repository' : @model.model_name.param_key
render_403 unless public_send("can_share_#{object_name}?", @model)
render_403 unless public_send(:"can_share_#{object_name}?", @model)
render_403 if !@model.shareable_write? && update_params[:write_permissions].present?
end

View file

@ -2,15 +2,22 @@
module UserRolesHelper
def user_roles_collection(object, with_inherit: false)
permission_group = "#{object.class.name}Permissions".constantize
permissions = permission_group.constants.map { |const| permission_group.const_get(const) }
if object.respond_to?(:private_shared_with_read?) && object.private_shared_with_read?(current_team)
viewer_role = UserRole.find_predefined_viewer_role
roles = [[viewer_role.name, viewer_role.id]]
else
permission_group = "#{object.class.name}Permissions".constantize
permissions = permission_group.constants.map { |const| permission_group.const_get(const) }
roles = user_roles_subset_by_permissions(permissions).order(id: :asc).pluck(:name, :id)
end
roles = user_roles_subset_by_permissions(permissions).order(id: :asc).pluck(:name, :id)
if with_inherit
roles = [[t('access_permissions.reset'), 'reset',
t("access_permissions.partials.#{object.class.name.underscore}_member_field.reset_description")]] +
roles
end
roles
end

View file

@ -195,7 +195,7 @@ export default {
});
},
getRoles() {
axios.get(this.params.roles_path)
axios.get(this.params.object.urls.user_roles)
.then((response) => {
this.roles = response.data.data;
});

View file

@ -262,7 +262,7 @@ export default {
return roles;
},
getRoles() {
axios.get(this.params.roles_path)
axios.get(this.params.object.urls.user_roles)
.then((response) => {
this.roles = response.data.data;
});

View file

@ -43,8 +43,27 @@ module Assignable
.where('? = ANY(user_roles.permissions)', read_permission)
)
.distinct
shared_objects =
if klass.new.respond_to?(:shared_with?)
joins(team_shared_objects: :team)
.where(team_shared_objects: { team: teams })
.where(teams: { id: Team.with_granted_permissions(user, TeamPermissions::MANAGE) })
else
none
end
globally_shared_objects =
if klass.new.respond_to?(:permission_level)
where(permission_level: %i(shared_read shared_write))
else
none
end
where(id: with_granted_user_permissions.reselect(:id))
.or(where(id: with_granted_group_permissions.reselect(:id)))
.or(where(id: shared_objects.select(:id)))
.or(where(id: globally_shared_objects.select(:id)))
}
scope :managable_by_user, lambda { |user, teams = user.permission_team|
@ -87,8 +106,8 @@ module Assignable
User.where(id: direct_user_ids).or(User.where(id: group_user_ids)).or(User.where(id: team_user_ids))
end
def default_public_user_role_id
team_assignments.where(team_id: team.id).pick(:user_role_id)
def default_public_user_role_id(current_team = nil)
team_assignments.where(team_id: (current_team || team).id).pick(:user_role_id)
end
def has_permission_children?

View file

@ -57,16 +57,25 @@ module Shareable
Rails.logger.info('Not connected to database, skipping sharable model initialization.')
end
def can_manage_shared?(user)
globally_shared? ||
(shared_with?(user.current_team) && user.current_team.permission_granted?(user, TeamPermissions::MANAGE))
end
def shareable_write?
true
end
def private_shared_with?(team)
team_shared_objects.where(team: team).any?
team_shared_objects.exists?(team: team)
end
def private_shared_with_read?(team)
team_shared_objects.exists?(team: team, permission_level: :shared_read)
end
def private_shared_with_write?(team)
team_shared_objects.where(team: team, permission_level: :shared_write).any?
team_shared_objects.exists?(team: team, permission_level: :shared_write)
end
def i_shared?(team)

View file

@ -32,10 +32,6 @@ class Repository < RepositoryBase
before_save :sync_name_with_snapshots, if: :name_changed?
before_destroy :refresh_report_references_on_destroy, prepend: true
after_save :assign_globally_shared_inventories, if: -> { saved_change_to_permission_level? && globally_shared? }
after_save :unassign_globally_shared_inventories, if: -> { saved_change_to_permission_level? && !globally_shared? }
after_save :unassign_unshared_items, if: :saved_change_to_permission_level
after_save :unlink_unshared_items, if: -> { saved_change_to_permission_level? && !globally_shared? }
validates :name,
presence: true,
@ -178,37 +174,6 @@ class Repository < RepositoryBase
repository_rows.joins(:my_module_repository_rows).where(my_module_repository_rows: { my_module_id: my_module.id })
end
def unassign_unshared_items
return if shared_read? || shared_write?
MyModuleRepositoryRow.joins(my_module: { experiment: { project: :team } })
.joins(repository_row: :repository)
.where(repository_rows: { repository: self })
.where.not(my_module: { experiment: { projects: { team: team } } })
.where.not(my_module: { experiment: { projects: { team: teams_shared_with } } })
.destroy_all
end
def unlink_unshared_items
repository_rows_ids = repository_rows.select(:id)
rows_to_unlink = RepositoryRow.joins("LEFT JOIN repository_row_connections \
ON repository_rows.id = repository_row_connections.parent_id \
OR repository_rows.id = repository_row_connections.child_id")
.where("repository_row_connections.parent_id IN (?) \
OR repository_row_connections.child_id IN (?)",
repository_rows_ids,
repository_rows_ids)
.joins(:repository)
.where.not(repositories: self)
.where.not(repositories: { team: team })
.distinct
RepositoryRowConnection.where(parent: repository_rows_ids, child: rows_to_unlink)
.destroy_all
RepositoryRowConnection.where(child: repository_rows_ids, parent: rows_to_unlink)
.destroy_all
end
def archived_branch?
archived?
end
@ -219,26 +184,6 @@ class Repository < RepositoryBase
repository_snapshots.update(name: name)
end
def assign_globally_shared_inventories
viewer_role = UserRole.find_by(name: UserRole.public_send('viewer_role').name)
normal_user_role = UserRole.find_by(name: UserRole.public_send('normal_user_role').name)
team_shared_objects.find_each(&:destroy!)
Team.where.not(id: team.id).find_each do |team|
team.users.find_each do |user|
team.repository_sharing_user_assignments.find_or_initialize_by(
user: user,
assignable: self
).update!(user_role: shared_write? ? normal_user_role : viewer_role)
end
end
end
def unassign_globally_shared_inventories
user_assignments.where.not(team: team).find_each(&:destroy!)
end
def refresh_report_references_on_destroy
report_elements.find_each do |report_element|
repository_snapshot = report_element.my_module

View file

@ -15,6 +15,7 @@ class Team < ApplicationRecord
after_create :generate_template_project
after_create :create_default_label_templates
after_create :create_default_repository_templates
scope :teams_select, -> { select(:id, :name).order(name: :asc) }
scope :ordered, -> { order('LOWER(name)') }
@ -57,22 +58,6 @@ class Team < ApplicationRecord
source: :shared_object,
source_type: 'RepositoryBase',
dependent: :destroy
has_many :repository_sharing_user_assignments,
(lambda do |team|
joins(
"INNER JOIN repositories "\
"ON user_assignments.assignable_type = 'RepositoryBase' "\
"AND user_assignments.assignable_id = repositories.id"
).where(team_id: team.id)
.where.not('user_assignments.team_id = repositories.team_id')
end),
class_name: 'UserAssignment',
dependent: :destroy
has_many :shared_by_user_repositories,
through: :repository_sharing_user_assignments,
source: :assignable,
source_type: 'RepositoryBase',
dependent: :destroy
has_many :shareable_links, inverse_of: :team, dependent: :destroy
has_many :storage_locations, dependent: :destroy
has_many :forms, dependent: :destroy

View file

@ -3,11 +3,6 @@
class TeamSharedObject < ApplicationRecord
enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS.except(:not_shared)
after_create :assign_shared_inventories, if: -> { shared_object.is_a?(Repository) }
before_destroy :unlink_unshared_items, if: -> { shared_object.is_a?(Repository) }
before_destroy :unassign_unshared_items, if: -> { shared_object.is_a?(Repository) }
before_destroy :unassign_unshared_inventories, if: -> { shared_object.is_a?(Repository) }
belongs_to :team
belongs_to :shared_object, polymorphic: true, inverse_of: :team_shared_objects
belongs_to :shared_repository,
@ -21,66 +16,10 @@ class TeamSharedObject < ApplicationRecord
validates :permission_level, presence: true
validates :shared_object_type, uniqueness: { scope: %i(shared_object_id team_id) }
validate :team_cannot_be_the_same
validate :not_globally_shared, if: -> { shared_object.is_a?(Repository) }
private
def team_cannot_be_the_same
errors.add(:team_id, :same_team) if shared_object.team.id == team_id
end
def not_globally_shared
errors.add(:shared_object_id, :is_globally_shared) if shared_object.globally_shared?
end
def assign_shared_inventories
team.user_assignments.find_each do |user_assignment|
shared_object.user_assignments.create!(
user: user_assignment.user,
user_role: user_assignment.user_role,
team: team
)
end
end
def unassign_unshared_items
return if shared_object.shared_read? || shared_object.shared_write?
MyModuleRepositoryRow.joins(my_module: { experiment: { project: :team } })
.joins(repository_row: :repository)
.where(my_module: { experiment: { projects: { team: team } } })
.where(repository_rows: { repository: shared_object })
.destroy_all
end
def unassign_unshared_inventories
team.repository_sharing_user_assignments.where(assignable: shared_object).find_each(&:destroy!)
end
def unlink_unshared_items
# We keep all the other teams shared with and the repository's own team
teams_ids = shared_object.teams_shared_with.where.not(id: team).pluck(:id)
teams_ids << shared_object.team_id
repository_rows_ids = shared_object.repository_rows.select(:id)
rows_to_unlink = RepositoryRow.joins("LEFT JOIN repository_row_connections \
ON repository_rows.id = repository_row_connections.parent_id \
OR repository_rows.id = repository_row_connections.child_id")
.where("repository_row_connections.parent_id IN (?) \
OR repository_row_connections.child_id IN (?)",
repository_rows_ids,
repository_rows_ids)
.joins(:repository)
.where.not(repositories: { team: teams_ids })
.select(:id)
RepositoryRowConnection.where("(repository_row_connections.parent_id IN (?) \
AND repository_row_connections.child_id IN (?)) \
OR (repository_row_connections.parent_id IN (?) \
AND repository_row_connections.child_id IN (?))",
repository_rows_ids,
rows_to_unlink,
rows_to_unlink,
repository_rows_ids)
.destroy_all
end
end

View file

@ -27,6 +27,6 @@ class UserGroupAssignment < ApplicationRecord
private
def set_assignable_team
self.team = assignable.team
self.team ||= assignable.team
end
end

View file

@ -8,7 +8,8 @@ Canaid::Permissions.register_for(RepositoryBase) do
# If original repository is deleted, snapshot ownership should be transferred to task
(!original_repository || original_repository.permission_granted?(user, RepositoryPermissions::READ)) && can_read_my_module?(user, repository.my_module)
else
repository.permission_granted?(user, RepositoryPermissions::READ)
repository.can_manage_shared?(user) ||
repository.permission_granted?(user, RepositoryPermissions::READ)
end
end
@ -131,7 +132,8 @@ Canaid::Permissions.register_for(Repository) do
end
can :manage_repository_users do |user, repository|
repository.permission_granted?(user, RepositoryPermissions::USERS_MANAGE)
repository.can_manage_shared?(user) ||
repository.permission_granted?(user, RepositoryPermissions::USERS_MANAGE)
end
end

View file

@ -85,6 +85,7 @@ module Lists
}
if can_manage_project_users?(object.project)
urls_list[:user_roles] = user_roles_access_permissions_experiment_path(object)
urls_list[:update_access] = access_permissions_experiment_path(object)
urls_list[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id)
end

View file

@ -73,6 +73,7 @@ module Lists
urls_list[:show] = form_path(object) if can_read_form?(object)
if can_manage_form_users?(object)
urls_list[:user_roles] = user_roles_access_permissions_form_path(object)
urls_list[:update_access] = access_permissions_form_path(object)
urls_list[:new_access] = new_access_permissions_form_path(id: object.id)
urls_list[:create_access] = access_permissions_forms_path(id: object.id)

View file

@ -74,7 +74,11 @@ module Lists
unfavorite: unfavorite_my_module_url(object)
}
urls_list[:update_access] = access_permissions_my_module_path(object) if can_manage_project_users?(object.experiment.project)
if can_manage_project_users?(object.experiment.project)
urls_list[:user_roles] = user_roles_access_permissions_my_module_path(object)
urls_list[:update_access] = access_permissions_my_module_path(object)
end
urls_list[:update_due_date] = my_module_path(object, user, format: :json) if can_update_my_module_due_date?(object)
urls_list[:update_start_date] = my_module_path(object, user, format: :json) if can_update_my_module_start_date?(object)

View file

@ -162,6 +162,7 @@ module Lists
urls_list[:show_access] = access_permissions_project_path(object)
urls_list[:show_user_group_assignments_access] = show_user_group_assignments_access_permissions_project_path(object)
if project? && can_manage_project_users?(object)
urls_list[:user_roles] = user_roles_access_permissions_project_path(object)
urls_list[:assigned_users] = assigned_users_list_project_path(object)
urls_list[:update_access] = access_permissions_project_path(object)
urls_list[:new_access] = new_access_permissions_project_path(id: object.id)

View file

@ -117,6 +117,7 @@ module Lists
end
if can_manage_protocol_users?(object)
urls_list[:user_roles] = user_roles_access_permissions_protocol_path(object)
urls_list[:update_access] = access_permissions_protocol_path(object)
urls_list[:new_access] = new_access_permissions_protocol_path(id: object.id)
urls_list[:create_access] = access_permissions_protocols_path(id: object.id)

View file

@ -9,12 +9,16 @@ module Lists
attributes :name, :code, :nr_of_rows, :team, :created_at, :created_by, :archived_on, :archived_by,
:urls, :top_level_assignable, :default_public_user_role_id, :assigned_users, :permissions
def default_public_user_role_id
object.default_public_user_role_id(current_user.current_team)
end
def nr_of_rows
object[:repository_rows_count]
end
def team
object[:team_name]
current_user.current_team.name
end
def created_at
@ -70,6 +74,7 @@ module Lists
}
if can_manage_repository_users?(object)
urls[:user_roles] = user_roles_access_permissions_repository_path(object)
urls[:update_access] = access_permissions_repository_path(id: object)
urls[:new_access] = new_access_permissions_repository_path(id: object.id)
urls[:create_access] = access_permissions_repositories_path(id: object.id)

View file

@ -11,7 +11,6 @@
active-page-url="<%= experiments_path(project_id: @project, view_mode: :active) %>"
archived-page-url="<%= experiments_path(project_id: @project, view_mode: :archived) %>"
current-view-mode="<%= params[:view_mode] || :active %>"
user-roles-url="<%= user_roles_projects_path %>"
:archived="<%= @project.archived?%>"
/>
</div>

View file

@ -16,7 +16,6 @@
actions-url="<%= actions_toolbar_forms_url %>"
data-source="<%= forms_path(format: :json) %>"
create-url="<%= forms_path if can_create_forms?(current_team) %>"
user-roles-url="<%= user_roles_forms_path %>"
active-page-url="<%= forms_path(view_mode: :active) %>"
archived-page-url="<%= forms_path(view_mode: :archived) %>"
current-view-mode="<%= params[:view_mode] || :active %>"

View file

@ -14,8 +14,7 @@
current-view-mode="<%= view_mode %>"
assigned-users-url="<%= (assigned_users_experiment_path(@experiment) if can_designate_users_to_new_task?(@experiment)) %>"
current-user-id="<%= current_user.id %>"
users-filter-url="<%= users_filter_projects_path %>"v
user-roles-url="<%= user_roles_projects_path %>"
users-filter-url="<%= users_filter_projects_path %>"
:tags-colors="<%= Constants::TAG_COLORS.to_json %>"
project-name="<%= @experiment.project.name %>"
:statuses-list="<%= MyModuleStatus.all.order(:id).map{ |i| [i.id, i.name] }.to_json %>"

View file

@ -18,7 +18,6 @@
current-folder-id="<%= current_folder&.id %>"
create-url="<%= projects_path if can_create_projects?(current_team) %>"
create-folder-url="<%= project_folders_path if can_create_project_folders?(current_team) %>"
user-roles-url="<%= user_roles_projects_path %>"
folders-tree-url="<%= tree_project_folders_path(view_mode: params[:view_mode]) %>"
move-to-url="<%= move_to_project_folders_path %>"
/>
@ -26,5 +25,3 @@
<%= render 'shared/tiny_mce_packs' %>
<%= javascript_include_tag 'vue_projects_list' %>
</div>

View file

@ -34,7 +34,6 @@
:archived-page-url="'<%= protocols_path(view_mode: :archived) %>'"
current-view-mode="<%= params[:view_mode] || :active %>"
:docx-parser-enabled="<%= Protocol.docx_parser_enabled? %>"
user-roles-url="<%= user_roles_protocols_path %>"
:create-url="'<%= protocols_path if can_create_protocols_in_repository?(current_team) %>'"
users-filter-url="<%= users_filter_projects_path %>"
/>

View file

@ -19,7 +19,6 @@
active-page-url="<%= repositories_path %>"
archived-page-url="<%= repositories_path(view_mode: :archived) %>"
current-view-mode="<%= params[:view_mode] || :active %>"
user-roles-url="<%= user_roles_repositories_path %>"
/>
</div>
</div>

View file

@ -336,41 +336,26 @@ Rails.application.routes.draw do
end
namespace :access_permissions do
resources :projects, defaults: { format: 'json' } do
member do
get :show_user_group_assignments
get :unassigned_user_groups
end
end
resources :protocols, defaults: { format: 'json' } do
member do
get :show_user_group_assignments
get :unassigned_user_groups
end
end
resources :forms, defaults: { format: 'json' } do
member do
get :show_user_group_assignments
get :unassigned_user_groups
end
end
resources :repositories, defaults: { format: 'json' } do
member do
get :show_user_group_assignments
get :unassigned_user_groups
%i(projects protocols forms repositories).each do |resource|
resources resource do
member do
get :show_user_group_assignments
get :unassigned_user_groups
get :user_roles
end
end
end
resources :experiments, only: %i(show update edit) do
member do
get :user_roles
get :show_user_group_assignments
end
end
resources :my_modules, only: %i(show update edit) do
member do
get :user_roles
get :show_user_group_assignments
end
end