Add permission management to repositories [SCI-12029]

This commit is contained in:
Martin Artnik 2025-07-03 14:32:26 +02:00
parent 3544131b8b
commit 1693729b42
17 changed files with 170 additions and 30 deletions

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module AccessPermissions
class RepositoriesController < BaseController
private
def set_model
@model = current_team.repositories.includes(user_assignments: %i(user user_role)).find_by(id: params[:id])
render_404 unless @model
end
def check_manage_permissions
render_403 unless can_manage_repository_users?(@model)
end
def check_read_permissions
render_403 unless can_read_repository?(@model) || can_manage_team?(@model.team)
end
end
end

View file

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

View file

@ -18,6 +18,7 @@
@share="share" @share="share"
@create="newRepository = true" @create="newRepository = true"
@tableReloaded="reloadingTable = false" @tableReloaded="reloadingTable = false"
@access="access"
/> />
</div> </div>
<ConfirmationModal <ConfirmationModal
@ -75,6 +76,8 @@
confirm: 'e2e-BT-confirmSharingChangesModal-delete' confirm: 'e2e-BT-confirmSharingChangesModal-delete'
}" }"
></ConfirmationModal> ></ConfirmationModal>
<AccessModal v-if="accessModalParams" :params="accessModalParams"
@close="accessModalParams = null" @refresh="reloadingTable = true" />
</template> </template>
<script> <script>
@ -89,6 +92,8 @@ import DuplicateRepositoryModal from './modals/duplicate.vue';
import ShareObjectModal from '../shared/share_modal.vue'; import ShareObjectModal from '../shared/share_modal.vue';
import DataTable from '../shared/datatable/table.vue'; import DataTable from '../shared/datatable/table.vue';
import NameRenderer from './renderers/name.vue'; import NameRenderer from './renderers/name.vue';
import AccessModal from '../shared/access_modal/modal.vue';
import UsersRenderer from '../projects/renderers/users.vue';
export default { export default {
name: 'RepositoriesTable', name: 'RepositoriesTable',
@ -100,7 +105,9 @@ export default {
EditRepositoryModal, EditRepositoryModal,
DuplicateRepositoryModal, DuplicateRepositoryModal,
NameRenderer, NameRenderer,
ShareObjectModal ShareObjectModal,
AccessModal,
UsersRenderer
}, },
props: { props: {
dataSource: { dataSource: {
@ -125,11 +132,16 @@ export default {
archivedPageUrl: { archivedPageUrl: {
type: String, type: String,
required: true required: true
},
userRolesUrl: {
type: String,
required: true
} }
}, },
data() { data() {
return { return {
reloadingTable: false, reloadingTable: false,
accessModalParams: null,
exportRepository: null, exportRepository: null,
newRepository: false, newRepository: false,
editRepository: null, editRepository: null,
@ -186,8 +198,14 @@ export default {
field: 'created_at', field: 'created_at',
headerName: this.i18n.t('libraries.index.table.added_on'), headerName: this.i18n.t('libraries.index.table.added_on'),
sortable: true sortable: true
}, }, {
{ field: 'assigned_users',
headerName: this.i18n.t('repositories.index.table.access'),
sortable: true,
cellRenderer: 'UsersRenderer',
minWidth: 210,
notSelectable: true
}, {
field: 'created_by', field: 'created_by',
headerName: this.i18n.t('libraries.index.table.added_by'), headerName: this.i18n.t('libraries.index.table.added_by'),
sortable: true sortable: true
@ -252,6 +270,12 @@ export default {
HelperModule.flashAlertMsg(error.response.data.error, 'danger'); HelperModule.flashAlertMsg(error.response.data.error, 'danger');
}); });
}, },
access(_event, rows) {
this.accessModalParams = {
object: rows[0],
roles_path: this.userRolesUrl
};
},
async deleteRepository(event, rows) { async deleteRepository(event, rows) {
const [repository] = rows; const [repository] = rows;
this.deleteModal.e2eAttributes = { this.deleteModal.e2eAttributes = {

View file

@ -12,8 +12,6 @@ module UserAssignments
assign_to_experiment(object) assign_to_experiment(object)
when MyModule when MyModule
assign_to_my_module(object) assign_to_my_module(object)
when Repository
assign_to_repository(object)
when Protocol when Protocol
assign_to_protocol(object) assign_to_protocol(object)
when Report when Report
@ -54,13 +52,6 @@ module UserAssignments
end end
end end
def assign_to_repository(repository)
team = repository.team
team.user_assignments.find_each do |assignment|
create_or_update_assignment(assignment, repository)
end
end
def assign_to_protocol(protocol) def assign_to_protocol(protocol)
if protocol.parent_id && protocol.in_repository_draft? if protocol.parent_id && protocol.in_repository_draft?
Protocol.transaction(requires_new: true) do Protocol.transaction(requires_new: true) do

View file

@ -29,6 +29,7 @@ class Repository < RepositoryBase
inverse_of: :original_repository inverse_of: :original_repository
has_many :repository_ledger_records, as: :reference, dependent: :nullify has_many :repository_ledger_records, as: :reference, dependent: :nullify
has_many :repository_table_filters, dependent: :destroy has_many :repository_table_filters, dependent: :destroy
has_many :users, through: :user_assignments
before_save :sync_name_with_snapshots, if: :name_changed? before_save :sync_name_with_snapshots, if: :name_changed?
before_destroy :refresh_report_references_on_destroy, prepend: true before_destroy :refresh_report_references_on_destroy, prepend: true
@ -70,6 +71,14 @@ class Repository < RepositoryBase
active.where(id: (readable_ids + shared_with_team_ids + globally_shared_ids).uniq) active.where(id: (readable_ids + shared_with_team_ids + globally_shared_ids).uniq)
} }
def top_level_assignable
true
end
def has_permission_children?
false
end
def readable_by_user?(user) def readable_by_user?(user)
permission_granted?(user, RepositoryPermissions::READ) permission_granted?(user, RepositoryPermissions::READ)
end end

View file

@ -127,6 +127,10 @@ Canaid::Permissions.register_for(Repository) do
can :manage_repository_stock do |user, repository| can :manage_repository_stock do |user, repository|
RepositoryBase.stock_management_enabled? && can_manage_repository_rows?(user, repository) RepositoryBase.stock_management_enabled? && can_manage_repository_rows?(user, repository)
end end
can :manage_repository_users do |user, repository|
repository.permission_granted?(user, RepositoryPermissions::USERS_MANAGE)
end
end end
Canaid::Permissions.register_for(RepositoryColumn) do Canaid::Permissions.register_for(RepositoryColumn) do

View file

@ -78,8 +78,6 @@ module Lists
urls_list[:create_access] = access_permissions_forms_path(id: object.id) urls_list[:create_access] = access_permissions_forms_path(id: object.id)
urls_list[:unassigned_user_groups] = unassigned_user_groups_access_permissions_form_path(id: object.id) urls_list[:unassigned_user_groups] = unassigned_user_groups_access_permissions_form_path(id: object.id)
urls_list[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id) urls_list[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id)
urls_list[:default_public_user_role_path] =
update_default_public_user_role_access_permissions_form_path(object)
end end
urls_list urls_list

View file

@ -168,8 +168,6 @@ module Lists
urls_list[:unassigned_user_groups] = unassigned_user_groups_access_permissions_project_path(id: object.id) urls_list[:unassigned_user_groups] = unassigned_user_groups_access_permissions_project_path(id: object.id)
urls_list[:create_access] = access_permissions_projects_path(id: object.id) urls_list[:create_access] = access_permissions_projects_path(id: object.id)
urls_list[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id) urls_list[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id)
urls_list[:default_public_user_role_path] =
update_default_public_user_role_access_permissions_project_path(object)
end end
urls_list urls_list

View file

@ -122,8 +122,6 @@ module Lists
urls_list[:create_access] = access_permissions_protocols_path(id: object.id) urls_list[:create_access] = access_permissions_protocols_path(id: object.id)
urls_list[:unassigned_user_groups] = unassigned_user_groups_access_permissions_protocol_path(id: object.id) urls_list[:unassigned_user_groups] = unassigned_user_groups_access_permissions_protocol_path(id: object.id)
urls_list[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id) urls_list[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id)
urls_list[:default_public_user_role_path] =
update_default_public_user_role_access_permissions_protocol_path(object)
end end
urls_list urls_list

View file

@ -6,7 +6,8 @@ module Lists
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
include ShareableSerializer include ShareableSerializer
attributes :name, :code, :nr_of_rows, :team, :created_at, :created_by, :archived_on, :archived_by, :urls attributes :name, :code, :nr_of_rows, :team, :created_at, :created_by, :archived_on, :archived_by,
:urls, :top_level_assignable, :assigned_users, :permissions
def nr_of_rows def nr_of_rows
object[:repository_rows_count] object[:repository_rows_count]
@ -32,16 +33,52 @@ module Lists
object[:archived_by_user] object[:archived_by_user]
end end
def urls def assigned_users
users = object.user_assignments.map do |ua|
{
avatar: avatar_path(ua.user, :icon_small),
full_name: ua.user_name_with_role
}
end
user_groups = object.user_group_assignments.map do |ua|
{
avatar: ActionController::Base.helpers.asset_path('icon/group.svg'),
full_name: ua.user_group_name_with_role
}
end
users + user_groups
end
def permissions
{ {
manage_users_assignments: can_manage_repository_users?(object)
}
end
def urls
urls = {
show: repository_path(object), show: repository_path(object),
update: team_repository_path(current_user.current_team, id: object, format: :json), update: team_repository_path(current_user.current_team, id: object, format: :json),
duplicate: team_repository_copy_path(current_user.current_team, repository_id: object, format: :json), duplicate: team_repository_copy_path(current_user.current_team, repository_id: object, format: :json),
shareable_teams: shareable_teams_team_shared_objects_path( shareable_teams: shareable_teams_team_shared_objects_path(
current_user.current_team, object_id: object.id, object_type: 'Repository' current_user.current_team, object_id: object.id, object_type: 'Repository'
), ),
show_access: access_permissions_repository_path(object),
share: team_shared_objects_path(current_user.current_team, object_id: object.id, object_type: 'Repository') share: team_shared_objects_path(current_user.current_team, object_id: object.id, object_type: 'Repository')
} }
if can_manage_repository_users?(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)
urls[:unassigned_user_groups] = unassigned_user_groups_access_permissions_project_path(id: object.id)
urls[:user_group_members] = users_users_settings_team_user_groups_path(team_id: object.team.id)
urls[:show_user_group_assignments_access] = show_user_group_assignments_access_permissions_repository_path(object)
end
urls
end end
end end
end end

View file

@ -60,7 +60,7 @@ class UserAssignmentSerializer < ActiveModel::Serializer
def user_assignment_resource_role_name(user, resource, inherit = '') def user_assignment_resource_role_name(user, resource, inherit = '')
user_assignment = resource.user_assignments.find_by(user: user) user_assignment = resource.user_assignments.find_by(user: user)
return '' if ([Project, Protocol].include?(resource.class) && inherit.blank?) || user_assignment.blank? return '' if ([Project, Protocol, Repository].any? { |c| resource.is_a?(c) } && inherit.blank?) || user_assignment.blank?
if user_assignment.automatically_assigned? && resource.permission_parent.present? if user_assignment.automatically_assigned? && resource.permission_parent.present?
parent = resource.permission_parent parent = resource.permission_parent

View file

@ -22,14 +22,25 @@ module Toolbars
return [] if @repositories.none? return [] if @repositories.none?
if @archived_state if @archived_state
[export_action, restore_action, delete_action] [access_action, export_action, restore_action, delete_action]
else else
[rename_action, duplicate_action, export_action, archive_action, share_action] [access_action, rename_action, duplicate_action, export_action, archive_action, share_action]
end.compact end.compact
end end
private private
def access_action
return unless @single
{
name: 'access',
label: I18n.t('general.access'),
icon: 'sn-icon sn-icon-project-member-access',
type: :emit
}
end
def rename_action def rename_action
return unless @single && can_manage_repository?(@repository) return unless @single && can_manage_repository?(@repository)

View file

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

View file

@ -612,7 +612,16 @@ class Extends
experiment_access_changed_user_group: 393, experiment_access_changed_user_group: 393,
my_module_access_changed_user_group: 394, my_module_access_changed_user_group: 394,
step_and_result_linked: 395, step_and_result_linked: 395,
step_and_result_unlinked: 396 step_and_result_unlinked: 396,
repository_access_granted: 397,
repository_access_changed: 398,
repository_access_revoked: 399,
repository_access_granted_all_team_members: 400,
repository_access_changed_all_team_members: 401,
repository_access_revoked_all_team_members: 402,
repository_access_granted_user_group: 403,
repository_access_changed_user_group: 404,
repository_access_revoked_user_group: 405
} }
ACTIVITY_GROUPS = { ACTIVITY_GROUPS = {
@ -626,7 +635,7 @@ class Extends
experiment: [*27..31, 57, 141, 165, *363..369, 393], experiment: [*27..31, 57, 141, 165, *363..369, 393],
reports: [48, 50, 49, 163, 164], reports: [48, 50, 49, 163, 164],
inventories: [70, 71, 105, 144, 145, 72, 73, 74, 102, 142, 143, 75, 76, 77, inventories: [70, 71, 105, 144, 145, 72, 73, 74, 102, 142, 143, 75, 76, 77,
78, 96, 107, 113, 114, *133..136, 180, 181, 182, *292..298, 308, 329], 78, 96, 107, 113, 114, *133..136, 180, 181, 182, *292..298, 308, 329, *397..405],
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82, protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82,
83, 101, 112, 123, 125, 117, 119, 129, 131, 187, 186, 83, 101, 112, 123, 125, 117, 119, 129, 131, 187, 186,
190, 191, *204..215, 220, 223, 227, 228, 229, *230..235, 190, 191, *204..215, 220, 223, 227, 228, 229, *230..235,

View file

@ -3253,6 +3253,7 @@ en:
no_libraries: no_libraries:
create_new_button: "New inventory" create_new_button: "New inventory"
create_new_button_tooltip: "Create new inventory" create_new_button_tooltip: "Create new inventory"
access: 'Access'
show: show:
head_title: "Inventories | %{library}" head_title: "Inventories | %{library}"
repository_row: repository_row:
@ -4609,6 +4610,8 @@ en:
mymodule_tooltip: "This role was set on this task" mymodule_tooltip: "This role was set on this task"
form_tooltip: "This role was set on this form." form_tooltip: "This role was set on this form."
form_tooltip_inherit: "This role was inherited from the form." form_tooltip_inherit: "This role was inherited from the form."
repository_tooltip: "This role was set on this inventory."
repository_tooltip_inherit: "This role was inherited from the inventory."
public_members_dropdown: public_members_dropdown:
title: "Members of team %{team}" title: "Members of team %{team}"
@ -4624,6 +4627,12 @@ en:
title: "Access to %{resource_name}" title: "Access to %{resource_name}"
edit_modal: edit_modal:
title: "Manage access for %{resource_name}" title: "Manage access for %{resource_name}"
repositories:
modals:
show_modal:
title: "Access to %{resource_name}"
edit_modal:
title: "Manage access for %{resource_name}"
experiments: experiments:
modals: modals:
show_modal: show_modal:

View file

@ -412,6 +412,15 @@ en:
step_and_result_unlinked_html: "%{user} unlinked result <strong>%{result}</strong> and step <strong>%{position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>." step_and_result_unlinked_html: "%{user} unlinked result <strong>%{result}</strong> and step <strong>%{position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
step_and_result_linked_html: "%{user} linked result <strong>%{result}</strong> and step <strong>%{step_position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>." step_and_result_linked_html: "%{user} linked result <strong>%{result}</strong> and step <strong>%{step_position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
step_and_result_unlinked_html: "%{user} unlinked result <strong>%{result}</strong> and step <strong>%{step_position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>." step_and_result_unlinked_html: "%{user} unlinked result <strong>%{result}</strong> and step <strong>%{step_position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
repository_access_granted_html: "%{user} granted access to %{user_target} with user role %{role} to inventory %{repository}."
repository_access_changed_html: "%{user} changed %{user_target}s role on inventory %{repository} to %{role}."
repository_access_revoked_html: "%{user} removed %{user_target} with user role %{role} from inventory %{repository}."
repository_access_granted_all_team_members_html: "%{user} granted access to all team members of %{team} team with user role %{role} to inventory %{repository}."
repository_access_changed_all_team_members_html: "%{user} changed %{team}s role on inventory %{repository} to %{role}."
repository_access_revoked_all_team_members_html: "%{user} removed %{team} team members with user role %{role} from inventory %{repository}."
repository_granted_user_group_html: "%{user} granted access to %{user_group} with user role %{role} to inventory template %{repository}."
repository_changed_user_group_html: "%{user} changed %{user_group}'s role on inventory template %{repository} to %{role}."
repository_revoked_user_group_html: "%{user} removed group %{user_group} with user role %{role} from inventory template %{repository}."
activity_name: activity_name:
create_project: "Project created" create_project: "Project created"
edit_project: "Project edited" edit_project: "Project edited"
@ -779,6 +788,15 @@ en:
my_module_access_changed_user_group: "Change role of group" my_module_access_changed_user_group: "Change role of group"
step_and_result_linked: "Task Result and Task protocol step linked" step_and_result_linked: "Task Result and Task protocol step linked"
step_and_result_unlinked: "Task Result and Task protocol step unlinked" step_and_result_unlinked: "Task Result and Task protocol step unlinked"
repository_access_granted: "User granted access to inventory"
repository_access_changed: "User role changed on inventory"
repository_access_revoked: "User removed from inventory"
repository_access_granted_all_team_members: "Grant access to all team members"
repository_access_changed_all_team_members: "Change role of all team members"
repository_access_revoked_all_team_members: "Remove access from all team members"
repository_access_granted_user_group: "Grant access to group"
repository_access_changed_user_group: "Change role of group"
repository_access_revoked_user_group: "Remove access to group"
activity_group: activity_group:
projects: "Projects" projects: "Projects"
task_results: "Task results" task_results: "Task results"

View file

@ -340,7 +340,6 @@ Rails.application.routes.draw do
member do member do
get :show_user_group_assignments get :show_user_group_assignments
get :unassigned_user_groups get :unassigned_user_groups
put :update_default_public_user_role
end end
end end
@ -348,7 +347,6 @@ Rails.application.routes.draw do
member do member do
get :show_user_group_assignments get :show_user_group_assignments
get :unassigned_user_groups get :unassigned_user_groups
put :update_default_public_user_role
end end
end end
@ -356,7 +354,13 @@ Rails.application.routes.draw do
member do member do
get :show_user_group_assignments get :show_user_group_assignments
get :unassigned_user_groups get :unassigned_user_groups
put :update_default_public_user_role end
end
resources :repositories, defaults: { format: 'json' } do
member do
get :show_user_group_assignments
get :unassigned_user_groups
end end
end end
@ -795,6 +799,9 @@ Rails.application.routes.draw do
get :repository_users get :repository_users
get :load_table get :load_table
end end
collection do
get :user_roles
end
# Save repository table state # Save repository table state
post 'state_save', post 'state_save',
to: 'user_repositories#save_table_state', to: 'user_repositories#save_table_state',