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 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)
export_repositories list user_roles)
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)
export_repositories list user_roles)
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)
@ -482,6 +483,10 @@ class RepositoriesController < ApplicationController
}
end
def user_roles
render json: { data: user_roles_collection(Repository.new).map(&:reverse) }
end
private
def load_repository

View file

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

View file

@ -12,8 +12,6 @@ module UserAssignments
assign_to_experiment(object)
when MyModule
assign_to_my_module(object)
when Repository
assign_to_repository(object)
when Protocol
assign_to_protocol(object)
when Report
@ -54,13 +52,6 @@ module UserAssignments
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)
if protocol.parent_id && protocol.in_repository_draft?
Protocol.transaction(requires_new: true) do

View file

@ -29,6 +29,7 @@ class Repository < RepositoryBase
inverse_of: :original_repository
has_many :repository_ledger_records, as: :reference, dependent: :nullify
has_many :repository_table_filters, dependent: :destroy
has_many :users, through: :user_assignments
before_save :sync_name_with_snapshots, if: :name_changed?
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)
}
def top_level_assignable
true
end
def has_permission_children?
false
end
def readable_by_user?(user)
permission_granted?(user, RepositoryPermissions::READ)
end

View file

@ -127,6 +127,10 @@ Canaid::Permissions.register_for(Repository) do
can :manage_repository_stock do |user, repository|
RepositoryBase.stock_management_enabled? && can_manage_repository_rows?(user, repository)
end
can :manage_repository_users do |user, repository|
repository.permission_granted?(user, RepositoryPermissions::USERS_MANAGE)
end
end
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[: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[:default_public_user_role_path] =
update_default_public_user_role_access_permissions_form_path(object)
end
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[: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[:default_public_user_role_path] =
update_default_public_user_role_access_permissions_project_path(object)
end
urls_list

View file

@ -122,8 +122,6 @@ module Lists
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[: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
urls_list

View file

@ -6,7 +6,8 @@ module Lists
include Rails.application.routes.url_helpers
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
object[:repository_rows_count]
@ -32,16 +33,52 @@ module Lists
object[:archived_by_user]
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),
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),
shareable_teams: shareable_teams_team_shared_objects_path(
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')
}
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

View file

@ -60,7 +60,7 @@ class UserAssignmentSerializer < ActiveModel::Serializer
def user_assignment_resource_role_name(user, resource, inherit = '')
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?
parent = resource.permission_parent

View file

@ -22,14 +22,25 @@ module Toolbars
return [] if @repositories.none?
if @archived_state
[export_action, restore_action, delete_action]
[access_action, export_action, restore_action, delete_action]
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
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
return unless @single && can_manage_repository?(@repository)

View file

@ -19,6 +19,7 @@
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

@ -612,7 +612,16 @@ class Extends
experiment_access_changed_user_group: 393,
my_module_access_changed_user_group: 394,
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 = {
@ -626,7 +635,7 @@ class Extends
experiment: [*27..31, 57, 141, 165, *363..369, 393],
reports: [48, 50, 49, 163, 164],
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,
83, 101, 112, 123, 125, 117, 119, 129, 131, 187, 186,
190, 191, *204..215, 220, 223, 227, 228, 229, *230..235,

View file

@ -3253,6 +3253,7 @@ en:
no_libraries:
create_new_button: "New inventory"
create_new_button_tooltip: "Create new inventory"
access: 'Access'
show:
head_title: "Inventories | %{library}"
repository_row:
@ -4609,6 +4610,8 @@ en:
mymodule_tooltip: "This role was set on this task"
form_tooltip: "This role was set on this 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:
title: "Members of team %{team}"
@ -4624,6 +4627,12 @@ en:
title: "Access to %{resource_name}"
edit_modal:
title: "Manage access for %{resource_name}"
repositories:
modals:
show_modal:
title: "Access to %{resource_name}"
edit_modal:
title: "Manage access for %{resource_name}"
experiments:
modals:
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_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>."
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:
create_project: "Project created"
edit_project: "Project edited"
@ -779,6 +788,15 @@ en:
my_module_access_changed_user_group: "Change role of group"
step_and_result_linked: "Task Result and Task protocol step linked"
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:
projects: "Projects"
task_results: "Task results"

View file

@ -340,7 +340,6 @@ Rails.application.routes.draw do
member do
get :show_user_group_assignments
get :unassigned_user_groups
put :update_default_public_user_role
end
end
@ -348,7 +347,6 @@ Rails.application.routes.draw do
member do
get :show_user_group_assignments
get :unassigned_user_groups
put :update_default_public_user_role
end
end
@ -356,7 +354,13 @@ Rails.application.routes.draw do
member do
get :show_user_group_assignments
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
@ -795,6 +799,9 @@ Rails.application.routes.draw do
get :repository_users
get :load_table
end
collection do
get :user_roles
end
# Save repository table state
post 'state_save',
to: 'user_repositories#save_table_state',