このコミットが含まれているのは:
@@ -4,7 +4,7 @@ require 'digest'
|
||||
class HistoryAccess < ActiveSupport::CurrentAttributes
|
||||
attribute :user
|
||||
class Forbidden < StandardError; end
|
||||
ARTIFACT_POLICY = 'counts-masked-summary-1'.freeze
|
||||
ARTIFACT_POLICY = 'counts-masked-summary-2'.freeze
|
||||
|
||||
KEYS = { 'post_versions' => ['post_id'], 'tag_versions' => ['tag_id'],
|
||||
'nico_tag_versions' => ['tag_id'],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
class CountsAxes
|
||||
MAX_AXES = 8
|
||||
MASKED_VALUE = '__counts_private__'.freeze
|
||||
PUBLIC_FIELDS = ['created_at', 'event_type'].freeze
|
||||
PUBLIC_FIELDS = ['resource_id', 'version_no', 'event_type', 'user',
|
||||
'created_at'].freeze
|
||||
class TooManyAxes < ArgumentError; end
|
||||
class PrivateCondition < ArgumentError; end
|
||||
|
||||
|
||||
@@ -192,7 +192,88 @@ class CountsPopulation
|
||||
|
||||
def apply_condition(entry)
|
||||
@condition ||= CountsCondition.new(self)
|
||||
@scope = @condition.apply(@scope, entry)
|
||||
unless summary_private_condition?(entry)
|
||||
@scope = @condition.apply(@scope, entry)
|
||||
return
|
||||
end
|
||||
|
||||
return if masked_noop?(entry)
|
||||
|
||||
# Evaluate private predicates only on authorised versions. On other versions,
|
||||
# the entire field is the same opaque marker, regardless of its stored value.
|
||||
original = @scope
|
||||
visible = private_visibility_sql(entry['field'])
|
||||
authorised = authorised_private_condition(original.where(Arel.sql(visible)), entry)
|
||||
@scope = if masked_match?(entry)
|
||||
authorised.or(original.where(Arel.sql("NOT (#{ visible })")))
|
||||
else
|
||||
authorised
|
||||
end
|
||||
end
|
||||
|
||||
def authorised_private_condition(scope, entry)
|
||||
marker = '********'
|
||||
if entry['operator'] == 'axis_eq'
|
||||
return scope.none if entry['value'] == marker &&
|
||||
definition.fields[entry['field']] != :string
|
||||
|
||||
return @condition.apply(scope, entry)
|
||||
end
|
||||
|
||||
field = definition.filter_metadata.find { _1[:key] == entry['field'] }
|
||||
return @condition.apply(scope, entry) unless field
|
||||
return @condition.apply(scope, entry) if field[:type] == :string &&
|
||||
field[:set_type] != 'tag_reference'
|
||||
|
||||
if entry['value'] == marker
|
||||
return scope.none if ['eq', 'gt', 'ge', 'lt', 'le', 'range'].include?(entry['operator'])
|
||||
return scope if entry['operator'] == 'ne'
|
||||
end
|
||||
if entry['values'].is_a?(Array) && entry['values'].include?(marker)
|
||||
return scope.none if entry['operator'] == 'all'
|
||||
|
||||
return @condition.apply(scope, entry.merge('values' => entry['values'] - [marker]))
|
||||
end
|
||||
@condition.apply(scope, entry)
|
||||
end
|
||||
|
||||
def summary_private_condition?(entry)
|
||||
params.fetch('mode', 'summary') == 'summary' && !@authorisation_flg &&
|
||||
!HistoryAccess.admin? && !CountsAxes::PUBLIC_FIELDS.include?(entry['field']) &&
|
||||
entry['field'] != 'user.id'
|
||||
end
|
||||
|
||||
def private_visibility_sql(field)
|
||||
visible = HistoryAccess.sql(definition.model, 'v')
|
||||
return visible unless field == 'changed_field'
|
||||
|
||||
"(#{ visible }) AND (#{ HistoryAccess.sql(definition.model, 'previous') })"
|
||||
end
|
||||
|
||||
def masked_noop?(entry)
|
||||
return false if entry['operator'] == 'axis_eq' || entry['blank']
|
||||
set = ['in', 'not_in', 'any', 'all', 'none'].include?(entry['operator'])
|
||||
return !entry.key?('values') if set
|
||||
|
||||
[entry['value'], entry['end']].all? { _1.nil? || _1 == '' }
|
||||
end
|
||||
|
||||
def masked_match?(entry)
|
||||
marker = '********'
|
||||
return false if entry['blank']
|
||||
|
||||
value = entry['value']
|
||||
case entry['operator']
|
||||
when 'axis_eq', 'eq' then value == marker
|
||||
when 'ne' then value != marker
|
||||
when 'like'
|
||||
pattern = Regexp.escape(value.to_s).gsub('%', '.*').gsub('_', '.')
|
||||
/\A#{ pattern }\z/i.match?(marker)
|
||||
when 'in', 'any' then entry.fetch('values', []).include?(marker)
|
||||
when 'not_in', 'none' then !entry.fetch('values', []).include?(marker)
|
||||
when 'all' then entry.fetch('values', []).all? { _1 == marker }
|
||||
else false
|
||||
end
|
||||
end
|
||||
|
||||
def snapshot_membership(value)
|
||||
@@ -209,6 +290,10 @@ class CountsPopulation
|
||||
def apply_tags
|
||||
raise ArgumentError unless definition.resource == 'posts'
|
||||
|
||||
private = params.fetch('mode', 'summary') == 'summary' && !@authorisation_flg &&
|
||||
!HistoryAccess.admin?
|
||||
original = @scope if private
|
||||
@scope = HistoryAccess.visible(@scope, 'v') if private
|
||||
if params['tag'].present?
|
||||
@scope = @scope.where(snapshot_membership(
|
||||
"#{ params.fetch('kind', 'internal') }:#{ params['tag'] }"))
|
||||
@@ -232,9 +317,21 @@ class CountsPopulation
|
||||
expression = parts.empty? ? 'FALSE' : "(#{ parts.join(' OR ') })"
|
||||
literal[:negative] ? "NOT #{ expression }" : expression
|
||||
end
|
||||
return if predicates.empty?
|
||||
if predicates.empty?
|
||||
@scope = original if private && params['tag'].blank?
|
||||
return
|
||||
end
|
||||
|
||||
any = params['match'] == 'any' || raw.split.any? { _1.downcase == 'type:or' }
|
||||
@scope = @scope.where(Arel.sql("(#{ predicates.join(any ? ' OR ' : ' AND ') })"))
|
||||
return unless private && params['tag'].blank?
|
||||
|
||||
# A masked tag collection cannot match a real tag. Negative terms match
|
||||
# the opaque marker independently of any hidden snapshot membership.
|
||||
hidden = any ? literals.any? { _1[:negative] } : literals.all? { _1[:negative] }
|
||||
if hidden
|
||||
visible = HistoryAccess.sql(definition.model, 'v')
|
||||
@scope = @scope.or(original.where(Arel.sql("NOT (#{ visible })")))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -220,19 +220,12 @@ class CountsTableQuery
|
||||
|
||||
private
|
||||
|
||||
# Public timeline predicates may include hidden records. Private predicates must
|
||||
# never become a count oracle, even when the selected axes themselves are public.
|
||||
# Summary predicates are evaluated against public coordinates or the masked
|
||||
# value by CountsPopulation. Never accept a private server-only coordinate in
|
||||
# user-supplied ordering/measure state.
|
||||
def validate_summary_conditions!
|
||||
return if mode != 'summary' || HistoryAccess.admin?
|
||||
|
||||
fields = QueryState.records(@params, 'conditions') + QueryState.records(@params, 'drill')
|
||||
private_fields = fields.any? { !CountsAxes::PUBLIC_FIELDS.include?(_1['field']) }
|
||||
keys = ['tags', 'tag', 'kind', 'resource_id', 'user_id']
|
||||
private_parameters = keys.any? do |key|
|
||||
@params.any? { |name, value| (name == key || name.start_with?("#{ key }.")) && value != '' }
|
||||
end
|
||||
raise CountsAxes::PrivateCondition if private_fields || private_parameters
|
||||
|
||||
['column_order', 'measures'].each do |key|
|
||||
# Coordinates are compared only to already masked expressions, never raw
|
||||
# values. Public coordinates remain sortable; a guessed private ID cannot match.
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする