このコミットが含まれているのは:
@@ -0,0 +1,151 @@
|
||||
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
|
||||
end
|
||||
新しいイシューから参照
ユーザーをブロックする