Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 1199 add sharing permisions to sample types #1953

Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
5793a5b
Add migration for sample_type_auth_lookup table
kdp-cloud Jul 3, 2024
53adc21
Typo
kdp-cloud Jul 3, 2024
08424e2
Apply migration to schema
kdp-cloud Jul 3, 2024
45c905b
Implementation of acts_as_asset
kdp-cloud Jul 3, 2024
089d33b
Write upgrade task
kdp-cloud Jul 3, 2024
c91dd6c
Add missing routes
kdp-cloud Jul 3, 2024
edfb8f6
Keep Rubocop happy
kdp-cloud Jul 3, 2024
869abfa
Include the assets methods and routes
kdp-cloud Jul 3, 2024
f025c5b
Add manage page
kdp-cloud Jul 3, 2024
8641699
Make sure contributors don't get a double permission when they're als…
kdp-cloud Jul 3, 2024
2bb4864
Can't access manage page of ISA-JSON compliant sample types
kdp-cloud Jul 3, 2024
080a4c8
Make sure user can't manage_update ISA-JSON compliant sample types
kdp-cloud Jul 3, 2024
dcd8e57
Change is_isa_json_compliant
kdp-cloud Jul 3, 2024
ff53254
Remove comment out test
kdp-cloud Jul 3, 2024
cadf689
Convert into before_action
kdp-cloud Jul 3, 2024
4a26951
Add controller test for accessing the manage page
kdp-cloud Jul 3, 2024
2a824fc
Change ISA-JSON compliant factories
kdp-cloud Jul 9, 2024
0777ba8
Add collections as resources
kdp-cloud Jul 9, 2024
a364e38
Add has_content_blobs concern
kdp-cloud Jul 9, 2024
bc3d9e7
Remove custom auth methods
kdp-cloud Jul 9, 2024
7a80dbc
Fix controller tests
kdp-cloud Jul 9, 2024
0aca7dd
custom related investigations instead of association through studies
kdp-cloud Jul 10, 2024
2763620
Skip action if is SampleType
kdp-cloud Jul 10, 2024
ef7fe5f
Fix sample type tests
kdp-cloud Jul 10, 2024
14103b5
Add policy and discussion_links to schema
kdp-cloud Jul 10, 2024
b21a9a2
Fix some more tests
kdp-cloud Jul 10, 2024
be30864
Add permissions partial
kdp-cloud Jul 10, 2024
5838a2b
Update sharing policies and tags
kdp-cloud Jul 10, 2024
4dd1be2
Add manage sample type button to show page
kdp-cloud Jul 10, 2024
8e93be0
Write controller test for managing sample types
kdp-cloud Jul 10, 2024
7c56386
Reuse attributes
kdp-cloud Jul 10, 2024
01385ea
Test for isa_study sample type permissions
kdp-cloud Jul 10, 2024
f4a912c
Simplify isa assays controller test
kdp-cloud Jul 10, 2024
60605a4
Test for isa_assay sample type permissions
kdp-cloud Jul 10, 2024
2a90c9b
Typo
kdp-cloud Jul 10, 2024
c116e8c
Update policies before linking to sample types
kdp-cloud Jul 24, 2024
7b0a873
Remove redundant method
kdp-cloud Sep 30, 2024
fd57830
Remove can_view and can_edit
kdp-cloud Sep 30, 2024
4c8d933
Contributor already has manage permission
kdp-cloud Sep 30, 2024
8f6a756
Better reflect current policies
kdp-cloud Sep 30, 2024
2b1f044
Comment out tests about referring samples
kdp-cloud Oct 1, 2024
b53d649
Merge branch 'main' into issue_1199_add_sharing_permisions_to_sample_…
kdp-cloud Oct 1, 2024
4c43636
Add missing end
kdp-cloud Oct 1, 2024
a7f8449
Add Timecop gem to make test more reproduceable
kdp-cloud Oct 1, 2024
e824fcf
Make the deleting of the blob consistent
kdp-cloud Oct 2, 2024
bbacf03
Revert "Add Timecop gem to make test more reproduceable"
kdp-cloud Oct 2, 2024
c88d4d2
Use ActiveSupport's Testing TimeHelpers
kdp-cloud Oct 2, 2024
7f1b1d3
Fix sample_controller_test
kdp-cloud Oct 8, 2024
b6ba110
Revert "Use ActiveSupport's Testing TimeHelpers"
kdp-cloud Oct 8, 2024
355d381
Rearrange before_actions
kdp-cloud Oct 9, 2024
c956119
Remove obsolete referring samples logic
kdp-cloud Oct 9, 2024
4402f51
Change tests to something more useful.
kdp-cloud Oct 9, 2024
c69f74b
Remove the destroy method to fall back to the default -> Fixes the fa…
kdp-cloud Oct 10, 2024
37bbd60
Fix `check_no_created_samples`
kdp-cloud Oct 10, 2024
8ba55a2
Reorder before_action `check_isa_json_compliance`
kdp-cloud Oct 10, 2024
50c3df4
clean up
kdp-cloud Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/controllers/isa_assays_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def new
end

def create
update_sharing_policies @isa_assay.assay
@isa_assay.sample_type.policy = @isa_assay.assay.policy
if @isa_assay.save
flash[:notice] = "The #{t('isa_assay')} was successfully created.<br/>".html_safe
respond_to do |format|
Expand All @@ -66,8 +68,9 @@ def edit
end

def update
update_sharing_policies @isa_assay.assay
@isa_assay.assay.attributes = isa_assay_params[:assay]

@isa_assay.sample_type.policy = @isa_assay.assay.policy
# update the sample_type
unless @isa_assay&.assay&.is_assay_stream?
if requested_item_authorized?(@isa_assay.sample_type)
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/isa_studies_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def new
def create
@isa_study = IsaStudy.new(isa_study_params)
update_sharing_policies @isa_study.study
@isa_study.source.policy = @isa_study.study.policy
@isa_study.sample_collection.policy = @isa_study.study.policy
@isa_study.source.contributor = User.current_user.person
@isa_study.sample_collection.contributor = User.current_user.person
@isa_study.study.sample_types = [@isa_study.source, @isa_study.sample_collection]
Expand Down Expand Up @@ -43,6 +45,8 @@ def update
# update the study
@isa_study.study.attributes = isa_study_params[:study]
update_sharing_policies @isa_study.study
@isa_study.source.policy = @isa_study.study.policy
@isa_study.sample_collection.policy = @isa_study.study.policy
update_relationships(@isa_study.study, isa_study_params[:study])

# update the source
Expand Down
100 changes: 58 additions & 42 deletions app/controllers/sample_types_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ class SampleTypesController < ApplicationController
respond_to :html, :json
include Seek::UploadHandling::DataUpload
include Seek::IndexPager
include Seek::AssetsCommon

before_action :samples_enabled?
before_action :find_sample_type, only: [:show, :edit, :update, :destroy, :template_details, :batch_upload]
before_action :find_sample_type, only: %i[show edit update destroy template_details batch_upload]
kdp-cloud marked this conversation as resolved.
Show resolved Hide resolved
before_action :check_no_created_samples, only: [:destroy]
before_action :find_assets, only: [:index]
before_action :auth_to_create, only: [:new, :create]
before_action :project_membership_required, only: [:create, :new, :select, :filter_for_select]
before_action :auth_to_create, only: %i[new create]
before_action :project_membership_required, only: %i[create new select filter_for_select]
before_action :check_isa_json_compliance, only: %i[edit update manage manage_update]

before_action :authorize_requested_sample_type, except: [:index, :new, :create]
before_action :find_and_authorize_requested_item, except: %i[index new create]

api_actions :index, :show, :create, :update, :destroy

Expand All @@ -19,7 +21,7 @@ class SampleTypesController < ApplicationController
def show
respond_to do |format|
format.html
format.json {render json: @sample_type, include: [params[:include]]}
format.json { render json: @sample_type, include: [params[:include]] }
end
end

Expand All @@ -28,7 +30,7 @@ def show
def new
@tab = 'manual'

attr = params["sample_type"] ? sample_type_params : {}
attr = params['sample_type'] ? sample_type_params : {}
@sample_type = SampleType.new(attr)
@sample_type.sample_attributes.build(is_title: true, required: true) # Initial attribute

Expand Down Expand Up @@ -62,17 +64,24 @@ def create
@sample_type = SampleType.new(sample_type_params)
@sample_type.contributor = User.current_user.person

# Update sharing policies
update_sharing_policies(@sample_type)
# Update relationships
update_relationships(@sample_type, params)
# Update tags
update_annotations(params[:tag_list], @sample_type)

# removes controlled vocabularies or linked seek samples where the type may differ
@sample_type.resolve_inconsistencies
@tab = 'manual'

respond_to do |format|
if @sample_type.save
format.html { redirect_to @sample_type, notice: 'Sample type was successfully created.' }
format.json { render json: @sample_type, status: :created, location: @sample_type, include: [params[:include]]}
format.json { render json: @sample_type, status: :created, location: @sample_type, include: [params[:include]] }
else
format.html { render action: 'new' }
format.json { render json: @sample_type.errors, status: :unprocessable_entity}
format.json { render json: @sample_type.errors, status: :unprocessable_entity }
end
end
end
Expand All @@ -83,13 +92,21 @@ def update

@sample_type.update(sample_type_params)
@sample_type.resolve_inconsistencies

# Update sharing policies
update_sharing_policies(@sample_type)
# Update relationships
update_relationships(@sample_type, params)
# Update tags
update_annotations(params[:tag_list], @sample_type)

respond_to do |format|
if @sample_type.save
format.html { redirect_to @sample_type, notice: 'Sample type was successfully updated.' }
format.json {render json: @sample_type, include: [params[:include]]}
format.json { render json: @sample_type, include: [params[:include]] }
else
format.html { render action: 'edit', status: :unprocessable_entity }
format.json { render json: @sample_type.errors, status: :unprocessable_entity}
format.json { render json: @sample_type.errors, status: :unprocessable_entity }
end
end
end
Expand All @@ -98,13 +115,26 @@ def update
# DELETE /sample_types/1.json
def destroy
respond_to do |format|
if @sample_type.can_delete? && @sample_type.destroy
format.html { redirect_to @sample_type,location: sample_types_path, notice: 'Sample type was successfully deleted.' }
format.json {render json: @sample_type, include: [params[:include]]}
else
format.html { redirect_to @sample_type, location: sample_types_path, notice: 'It was not possible to delete the sample type.' }
format.json { render json: @sample_type.errors, status: :unprocessable_entity}
if @sample_type.can_delete? && @sample_type.destroy
format.html do
redirect_to @sample_type, location: sample_types_path, notice: 'Sample type was successfully deleted.'
end
format.json { render json: @sample_type, include: [params[:include]] }
else
format.html do
redirect_to @sample_type, location: sample_types_path,
notice: 'It was not possible to delete the sample type.'
end
format.json { render json: @sample_type.errors, status: :unprocessable_entity }
end
end
end

def check_isa_json_compliance
@sample_type = SampleType.find(params[:id])
if Seek::Config.isa_json_compliance_enabled && @sample_type.is_isa_json_compliant?
flash[:error] = 'This sample type is ISA JSON compliant and cannot be managed.'
redirect_to sample_types_path
end
end

Expand Down Expand Up @@ -133,40 +163,39 @@ def filter_for_select
render partial: 'sample_types/select/filtered_sample_types'
end

def batch_upload

end
def batch_upload; end

private

def sample_type_params
attributes = params[:sample_type][:sample_attributes]
if (attributes)
if attributes
params[:sample_type][:sample_attributes_attributes] = []
attributes.each do |attribute|
if attribute[:sample_attribute_type]
if attribute[:sample_attribute_type][:id]
attribute[:sample_attribute_type_id] = attribute[:sample_attribute_type][:id].to_i
elsif attribute[:sample_attribute_type][:title]
attribute[:sample_attribute_type_id] = SampleAttributeType.where(title: attribute[:sample_attribute_type][:title]).first.id
attribute[:sample_attribute_type_id] =
SampleAttributeType.where(title: attribute[:sample_attribute_type][:title]).first.id
end
end
attribute[:unit_id] = Unit.where(symbol: attribute[:unit_symbol]).first.id unless attribute[:unit_symbol].nil?
params[:sample_type][:sample_attributes_attributes] << attribute
end
end

if (params[:sample_type][:assay_assets_attributes])
if params[:sample_type][:assay_assets_attributes]
params[:sample_type][:assay_ids] = params[:sample_type][:assay_assets_attributes].map { |x| x[:assay_id] }
end

params.require(:sample_type).permit(:title, :description, {tags: []}, :template_id, *creator_related_params,
params.require(:sample_type).permit(:title, :description, { tags: [] }, :template_id, *creator_related_params,
{ project_ids: [],
sample_attributes_attributes: [:id, :title, :pos, :required, :is_title,
:description, :pid, :sample_attribute_type_id,
:sample_controlled_vocab_id, :isa_tag_id,
:allow_cv_free_text, :linked_sample_type_id,
:unit_id, :_destroy] }, :assay_ids => [])
sample_attributes_attributes: %i[id title pos required is_title
description pid sample_attribute_type_id
sample_controlled_vocab_id isa_tag_id
allow_cv_free_text linked_sample_type_id
unit_id _destroy] }, assay_ids: [])
end


Expand All @@ -186,21 +215,8 @@ def find_sample_type
@sample_type = scope.find(params[:id])
end

#intercepts the standard 'find_and_authorize_requested_item' for additional special check for a referring_sample_id
def authorize_requested_sample_type
privilege = Seek::Permissions::Translator.translate(action_name)
return if privilege.nil?

if privilege == :view && params[:referring_sample_id].present?
@sample_type.can_view?(User.current_user,Sample.find_by_id(params[:referring_sample_id])) || find_and_authorize_requested_item
else
find_and_authorize_requested_item
end

end

def check_no_created_samples
if (count = @sample_type.samples.count) > 0
if (count = @sample_type.samples.count).positive?
flash[:error] = "Cannot #{action_name} this sample type - There are #{count} samples using it."
redirect_to @sample_type
end
Expand Down
36 changes: 11 additions & 25 deletions app/models/sample_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class SampleType < ApplicationRecord
acts_as_favouritable
has_external_identifier # to be replaced with acts_as_asset when sharing permissions are adding in upcoming pull request

acts_as_asset

has_many :samples, inverse_of: :sample_type

has_filter :contributor
Expand Down Expand Up @@ -61,6 +63,12 @@ class SampleType < ApplicationRecord

has_annotation_type :sample_type_tag, method_name: :tags

def investigations
return [] if studies.empty? && assays.empty?

(studies.map(&:investigation).compact << assays.map(&:investigation).compact).flatten.uniq
end

# Creates sample attributes from an ISA template.
# @param template [Template] The ISA template to create sample attributes from.
# @param linked_sample_type [SampleType, nil] The linked sample type, if any.
Expand Down Expand Up @@ -98,12 +106,14 @@ def next_linked_sample_types
end

def is_isa_json_compliant?
studies.any? || assays.any?
has_only_isa_json_compliant_investigations = studies.map(&:investigation).compact.all?(&:is_isa_json_compliant?) || assays.map(&:investigation).compact.all?(&:is_isa_json_compliant?)
(studies.any? || assays.any?) && has_only_isa_json_compliant_investigations && !isa_template.nil?
end

def validate_value?(attribute_name, value)
attribute = sample_attributes.detect { |attr| attr.title == attribute_name }
raise UnknownAttributeException, "Unknown attribute #{attribute_name}" unless attribute

attribute.validate_value?(value)
end

Expand Down Expand Up @@ -137,10 +147,6 @@ def resolve_inconsistencies
resolve_seek_samples_inconsistencies
end

def can_download?(user = User.current_user)
can_view?(user)
end

def self.user_creatable?
Sample.user_creatable?
end
Expand All @@ -150,18 +156,6 @@ def self.can_create?
can && (!Seek::Config.project_admin_sample_type_restriction || User.current_user.is_admin_or_project_administrator?)
end

def can_edit?(user = User.current_user)
return false if user.nil? || user.person.nil? || !Seek::Config.samples_enabled
return true if user.is_admin?

# Make the ISA JSON compliant sample types editable when a user is a project member instead of a project admin
if is_isa_json_compliant?
contributor == user.person || projects.detect { |project| project.has_member? user.person }.present?
else
contributor == user.person || projects.detect { |project| project.can_manage?(user) }.present?
end
end

def can_delete?(user = User.current_user)
# Users should be able to delete an ISA JSON compliant sample type that has linked sample attributes,
# as long as it's ISA JSON compliant.
Expand All @@ -176,14 +170,6 @@ def can_delete?(user = User.current_user)
end
end

def can_view?(user = User.current_user, referring_sample = nil, view_in_single_page = false)
return false if Seek::Config.isa_json_compliance_enabled && template_id.present? && !view_in_single_page

project_membership = user&.person && (user.person.projects & projects).any?
is_creator = creators.include?(user&.person)
project_membership || public_samples? || is_creator || check_referring_sample_permission(user, referring_sample)
end

def editing_constraints
Seek::Samples::SampleTypeEditingConstraints.new(self)
end
Expand Down
3 changes: 3 additions & 0 deletions app/views/sample_types/_buttons.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@
<% if sample_type.can_delete? %>
<%= delete_icon(sample_type, current_user) %>
<% end %>
<% if sample_type.can_manage? %>
<li><%= image_tag_for_key('manage', manage_sample_type_path(sample_type), nil, nil, "Manage Sample Type") -%></li>
<% end %>
<% end %>
4 changes: 1 addition & 3 deletions app/views/sample_types/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,14 @@
</div>

<%= render partial: "projects/project_selector", locals: { resource: @sample_type } -%>

<%= render partial: 'assets/manage_specific_attributes', locals: {f: f} if show_form_manage_specific_attributes? %>
<div class="tab-content">
<div role="tabpanel" class="tab-pane<%= ' active'.html_safe if tab == 'manual' -%>" id="manual">
<div class="form-group">
<%= f.label :tags -%>
<%= tags_input('sample_type[tags]', @sample_type.tags, typeahead: {type: 'sample_type_tag'}, class:'form-control') %>
</div>

<%= render partial: "assets/author_form", locals: { form: f } -%>

<h3>Attributes</h3>
<p class="help-block">Re-arrange attributes by clicking and dragging the button on the left-hand side of each row.</p>
<table class="table" id="attribute-table">
Expand Down
1 change: 1 addition & 0 deletions app/views/sample_types/manage.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render partial: 'assets/manage_form', locals: { resource: @sample_type, show_projects: false, sharing_link: false } %>
10 changes: 3 additions & 7 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@

### SAMPLE TYPES ###
#
resources :sample_types do
resources :sample_types, concerns: %i[asset has_content_blobs] do
collection do
post :create_from_template
get :select
Expand All @@ -703,14 +703,10 @@
member do
get :template_details
get :batch_upload
post :update_annotations_ajax
end
resources :samples
resources :content_blobs do
member do
get :download
end
end
resources :projects, :programmes, :templates, :studies, :assays, only: [:index]
resources :investigations, :people, :collections, :publications, :projects, :programmes, :templates, :studies, :assays, only: [:index]
end

### SAMPLE ATTRIBUTE TYPES ###
Expand Down
17 changes: 17 additions & 0 deletions db/migrate/20240703101126_create_sample_type_auth_lookup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class CreateSampleTypeAuthLookup < ActiveRecord::Migration[6.1]
def change
create_table :sample_type_auth_lookup do |t|
t.integer :user_id
t.integer :asset_id
t.boolean :can_view, :default => false
t.boolean :can_manage, :default => false
t.boolean :can_edit, :default => false
t.boolean :can_download, :default => false
t.boolean :can_delete, :default => false
t.timestamps
end
add_index :sample_type_auth_lookup, [:user_id, :asset_id, :can_view], :name => "index_sample_type_user_id_asset_id_can_view"
add_index :sample_type_auth_lookup, [:user_id, :can_view], :name => "index_sample_type_auth_lookup_on_user_id_and_can_view"
add_column :sample_types, :policy_id, :integer
end
end
Loading