#
# SPDX-License-Identifier: AGPL-3.0
+require 'arvados_model_updates'
require 'has_uuid'
require 'record_filters'
require 'serializers'
+require 'request_error'
class ArvadosModel < ActiveRecord::Base
self.abstract_class = true
+ include ArvadosModelUpdates
include CurrentApiClient # current_user, current_api_client, etc.
include DbCurrentTime
extend RecordFilters
class_name: 'Link',
primary_key: :uuid)
- class PermissionDeniedError < StandardError
+ class PermissionDeniedError < RequestError
def http_status
403
end
end
- class AlreadyLockedError < StandardError
+ class AlreadyLockedError < RequestError
def http_status
422
end
end
- class LockFailedError < StandardError
+ class LockFailedError < RequestError
def http_status
422
end
end
- class InvalidStateTransitionError < StandardError
+ class InvalidStateTransitionError < RequestError
def http_status
422
end
end
- class UnauthorizedError < StandardError
+ class UnauthorizedError < RequestError
def http_status
401
end
end
- class UnresolvableContainerError < StandardError
+ class UnresolvableContainerError < RequestError
def http_status
422
end
# Collect the UUIDs of the authorized users.
sql_table = kwargs.fetch(:table_name, table_name)
- include_trashed = kwargs.fetch(:include_trashed, false)
+ include_trash = kwargs.fetch(:include_trash, false)
- sql_conds = []
+ sql_conds = nil
user_uuids = users_list.map { |u| u.uuid }
- User.install_view('permission')
+ exclude_trashed_records = ""
+ if !include_trash and (sql_table == "groups" or sql_table == "collections") then
+ # Only include records that are not explicitly trashed
+ exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
+ end
- # Check if any of the users are admin.
if users_list.select { |u| u.is_admin }.any?
- if !include_trashed
- # exclude rows that are trashed.
- if self.column_names.include? "owner_uuid"
- sql_conds += ["NOT EXISTS(SELECT target_uuid
- FROM permission_view pv
- WHERE trashed = 1 AND
- (#{sql_table}.uuid = pv.target_uuid OR #{sql_table}.owner_uuid = pv.target_uuid))"]
- else
- sql_conds += ["NOT EXISTS(SELECT target_uuid
- FROM permission_view pv
- WHERE trashed = 1 AND
- (#{sql_table}.uuid = pv.target_uuid))"]
+ # Admin skips most permission checks, but still want to filter on trashed items.
+ if !include_trash
+ if sql_table != "api_client_authorizations"
+ # Only include records where the owner is not trashed
+ sql_conds = "NOT EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
+ "WHERE trashed = 1 AND "+
+ "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records}"
end
end
else
- # Match objects which appear in the permission view
- trash_clause = if !include_trashed then "trashed = 0 AND" else "" end
-
- # Match any object (evidently a group or user) whose UUID is
- # listed explicitly in user_uuids.
- sql_conds += ["#{sql_table}.uuid IN (:user_uuids)"]
-
- if self.column_names.include? "owner_uuid"
- sql_conds += ["EXISTS(SELECT target_uuid
- FROM permission_view pv
- WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 AND #{trash_clause}
- (#{sql_table}.uuid = pv.target_uuid OR
- (#{sql_table}.owner_uuid = pv.target_uuid AND pv.target_owner_uuid is NOT NULL)))"]
- # Match any object whose owner is listed explicitly in
- # user_uuids.
- trash_clause = if !include_trashed
- "1 NOT IN (SELECT trashed
- FROM permission_view pv
- WHERE #{sql_table}.uuid = pv.target_uuid) AND"
- else
- ""
- end
- sql_conds += ["(#{trash_clause} #{sql_table}.owner_uuid IN (:user_uuids))"]
- else
- sql_conds += ["EXISTS(SELECT target_uuid
- FROM permission_view pv
- WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 AND #{trash_clause}
- (#{sql_table}.uuid = pv.target_uuid))"]
+ trashed_check = ""
+ if !include_trash then
+ trashed_check = "AND trashed = 0"
+ end
+
+ # Note: it is possible to combine the direct_check and
+ # owner_check into a single EXISTS() clause, however it turns
+ # out query optimizer doesn't like it and forces a sequential
+ # table scan. Constructing the query with separate EXISTS()
+ # clauses enables it to use the index.
+ #
+ # see issue 13208 for details.
+
+ # Match a direct read permission link from the user to the record uuid
+ direct_check = "EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
+ "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_uuid = #{sql_table}.uuid)"
+
+ # Match a read permission link from the user to the record's owner_uuid
+ owner_check = ""
+ if sql_table != "api_client_authorizations" and sql_table != "groups" then
+ owner_check = "OR EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
+ "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_uuid = #{sql_table}.owner_uuid AND target_owner_uuid IS NOT NULL) "
end
+ links_cond = ""
if sql_table == "links"
# Match any permission link that gives one of the authorized
# users some permission _or_ gives anyone else permission to
# view one of the authorized users.
- sql_conds += ["(#{sql_table}.link_class IN (:permission_link_classes) AND "+
- "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"]
+ links_cond = "OR (#{sql_table}.link_class IN (:permission_link_classes) AND "+
+ "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
end
+
+ sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
+
end
- where(sql_conds.join(' OR '),
- user_uuids: user_uuids,
- permission_link_classes: ['permission', 'resources'])
+ self.where(sql_conds,
+ user_uuids: user_uuids,
+ permission_link_classes: ['permission', 'resources'])
end
def save_with_unique_name!
def self.full_text_searchable_columns
self.columns.select do |col|
- col.type == :string or col.type == :text
+ [:string, :text, :jsonb].include?(col.type)
end.map(&:name)
end
def self.full_text_tsvector
parts = full_text_searchable_columns.collect do |column|
- "coalesce(#{column},'')"
+ cast = serialized_attributes[column] ? '::text' : ''
+ "coalesce(#{column}#{cast},'')"
end
"to_tsvector('english', #{parts.join(" || ' ' || ")})"
end
self.updated_at = current_time
self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
self.modified_at = current_time
- self.modified_by_user_uuid = current_user ? current_user.uuid : nil
+ if !anonymous_updater
+ self.modified_by_user_uuid = current_user ? current_user.uuid : nil
+ end
self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
true
end
if self == ArvadosModel
# If called directly as ArvadosModel.find_by_uuid rather than via subclass,
# delegate to the appropriate subclass based on the given uuid.
- self.resource_class_for_uuid(uuid).unscoped.find_by_uuid(uuid)
+ self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
else
super
end