161 行
6.2 KiB
Ruby
161 行
6.2 KiB
Ruby
require 'digest'
|
|
|
|
# Request/worker authority comes only from a server-resolved user. Default is public.
|
|
class HistoryAccess < ActiveSupport::CurrentAttributes
|
|
attribute :user
|
|
class Forbidden < StandardError; end
|
|
|
|
KEYS = { 'post_versions' => ['post_id'], 'tag_versions' => ['tag_id'],
|
|
'nico_tag_versions' => ['tag_id'],
|
|
'tag_description_versions' => ['tag_id', 'language_code'],
|
|
'tag_group_versions' => ['tag_group_id'], 'material_versions' => ['material_id'] }.freeze
|
|
|
|
def self.revision
|
|
values = KEYS.keys.map do |table|
|
|
table.classify.constantize.unscoped.where(event_type: 'discard').maximum(:id)
|
|
end
|
|
Digest::SHA256.hexdigest(values.to_json)
|
|
end
|
|
|
|
def self.authority
|
|
{ 'policy_revision' => revision, 'admin' => admin?, 'actor_id' => user&.id }
|
|
end
|
|
|
|
def self.authorised_artifact?(metadata)
|
|
return false unless metadata['policy_revision'].is_a?(String)
|
|
if metadata['admin']
|
|
admin? && user.id == metadata['actor_id']
|
|
else
|
|
metadata['policy_revision'] == revision
|
|
end
|
|
end
|
|
|
|
def self.admin? = user&.admin? == true
|
|
|
|
def self.sql(model, row = model.table_name, references: true)
|
|
return 'TRUE' if admin?
|
|
|
|
table = model.table_name
|
|
keys = KEYS.fetch(table)
|
|
matches = keys.map { "boundary.#{ _1 } = #{ row }.#{ _1 }" }.join(' AND ')
|
|
clauses = ["NOT EXISTS (SELECT 1 FROM #{ table } boundary WHERE #{ matches } " +
|
|
"AND boundary.event_type = 'discard' AND boundary.version_no >= #{ row }.version_no)"]
|
|
clauses += reference_sql(table, row) if references
|
|
clauses.map { "(#{ _1 })" }.join(' AND ')
|
|
end
|
|
|
|
def self.discard_reference(table, key, expression)
|
|
"NOT EXISTS (SELECT 1 FROM #{ table } blocked WHERE blocked.event_type = 'discard' " +
|
|
"AND #{ expression.call("blocked.#{ key }") })"
|
|
end
|
|
|
|
def self.reference_sql(table, row)
|
|
case table
|
|
when 'post_versions'
|
|
[['tag_versions', 'tag_id', 'tag_id'],
|
|
['nico_tag_versions', 'tag_id', 'external_tag_id']].map do |target, key, field|
|
|
discard_reference(target, key, ->(id) {
|
|
"JSON_CONTAINS(#{ row }.tags_json, JSON_OBJECT('#{ field }', #{ id }))"
|
|
})
|
|
end + [discard_reference('post_versions', 'post_id', ->(id) {
|
|
"FIND_IN_SET(#{ id }, REPLACE(#{ row }.parent_post_ids, ' ', ',')) > 0"
|
|
})]
|
|
when 'tag_versions'
|
|
[discard_reference('tag_versions', 'tag_id', ->(id) {
|
|
"FIND_IN_SET(#{ id }, REPLACE(#{ row }.parent_tag_ids, ' ', ',')) > 0"
|
|
})]
|
|
when 'tag_description_versions'
|
|
[discard_reference('tag_versions', 'tag_id', ->(id) { "#{ row }.tag_id = #{ id }" })]
|
|
when 'material_versions'
|
|
[discard_reference('tag_versions', 'tag_id', ->(id) { "#{ row }.tag_id = #{ id }" }),
|
|
discard_reference('material_versions', 'material_id', ->(id) {
|
|
"#{ row }.parent_id = #{ id }"
|
|
})]
|
|
when 'tag_group_versions'
|
|
[['tag_versions', 'tag_id'], ['tag_group_versions', 'tag_group_id']].map do |target, field|
|
|
discard_reference(target, field, ->(id) {
|
|
['members', 'placements'].map do |column|
|
|
"JSON_CONTAINS(COALESCE(JSON_EXTRACT(#{ row }.#{ column }, " +
|
|
"'$**.#{ field }'), JSON_ARRAY()), CAST(#{ id } AS CHAR))"
|
|
end.join(' OR ')
|
|
})
|
|
end
|
|
else []
|
|
end
|
|
end
|
|
|
|
def self.visible(scope, row = scope.klass.table_name)
|
|
scope.where(Arel.sql(sql(scope.klass, row)))
|
|
end
|
|
|
|
def self.current(scope, version_model)
|
|
return scope if admin?
|
|
|
|
table = scope.klass.table_name
|
|
keys = KEYS.fetch(version_model.table_name)
|
|
matches = keys.map do |key|
|
|
column = scope.klass.column_names.include?(key) ? key : 'id'
|
|
"current_snapshot.#{ key } = #{ table }.#{ column }"
|
|
end.join(' AND ')
|
|
boundary = "#{ matches } AND current_snapshot.version_no >= #{ table }.version_no " +
|
|
"AND current_snapshot.event_type = 'discard'"
|
|
scope = scope.where(Arel.sql("NOT EXISTS (SELECT 1 FROM #{ version_model.table_name } " +
|
|
"current_snapshot WHERE #{ boundary })"))
|
|
condition = "#{ matches } AND current_snapshot.version_no = #{ table }.version_no"
|
|
scope.where(Arel.sql("NOT EXISTS (SELECT 1 FROM #{ version_model.table_name } " +
|
|
"current_snapshot WHERE #{ condition } AND NOT (#{ sql(version_model, 'current_snapshot') }))"))
|
|
end
|
|
|
|
def self.require_current!(record, version_model)
|
|
return if admin?
|
|
raise Forbidden unless current(record.class.where(id: record.id), version_model).exists?
|
|
end
|
|
|
|
def self.blob_visible?(blob, seen = [])
|
|
return true if admin?
|
|
return false if seen.include?(blob.id)
|
|
seen = seen + [blob.id]
|
|
private_versions = MaterialVersion.where(file_blob_id: blob.id)
|
|
.where(Arel.sql("NOT (#{ sql(MaterialVersion) })"))
|
|
return false if private_versions.exists?
|
|
|
|
blob.attachments.each do |attachment|
|
|
model, key = { 'Post' => [PostVersion, 'post_id'],
|
|
'Material' => [MaterialVersion, 'material_id'] }[attachment.record_type]
|
|
if model && model.where(key => attachment.record_id, event_type: 'discard').exists?
|
|
return false
|
|
end
|
|
if attachment.record_type == 'ActiveStorage::VariantRecord'
|
|
original = ActiveStorage::VariantRecord.find_by(id: attachment.record_id)&.blob
|
|
return false unless original && blob_visible?(original, seen)
|
|
end
|
|
end
|
|
true
|
|
end
|
|
|
|
def self.require_many!(records)
|
|
return if admin? || records.empty?
|
|
|
|
ids = records.map(&:id).uniq
|
|
raise Forbidden unless visible(records.first.class.where(id: ids)).count == ids.length
|
|
end
|
|
|
|
def self.require!(record)
|
|
return record if admin? || visible(record.class.where(id: record.id)).exists?
|
|
|
|
raise Forbidden
|
|
end
|
|
|
|
# This number deliberately ignores time, identity and content filters to prevent count probes.
|
|
def self.population_count(model) = model.unscoped.count
|
|
|
|
# Only the resource-wide denied count is public, never a condition-dependent denied count.
|
|
def self.population_counts(model)
|
|
total, excluded = model.unscoped.optimizer_hints('MAX_EXECUTION_TIME(10000)').pick(
|
|
Arel.sql('COUNT(*)'),
|
|
Arel.sql("COALESCE(SUM(CASE WHEN #{ sql(model) } THEN 0 ELSE 1 END), 0)"))
|
|
{ population_count: total.to_i, excluded_count: excluded.to_i,
|
|
excluded_count_scope: 'all_recorded_versions' }
|
|
end
|
|
end
|