Adding first cut of Redactions support

Redactions support hiding historical versions of elements and
collecting meta-data about that action together.
This commit is contained in:
Matt Amos 2012-03-28 12:37:40 +01:00 committed by Tom Hughes
parent 2063fc98f1
commit 67dd9e4c9d
20 changed files with 312 additions and 13 deletions

View file

@ -2,17 +2,26 @@ class OldNodeController < ApplicationController
require 'xml/libxml'
skip_before_filter :verify_authenticity_token
before_filter :authorize, :only => [ :redact ]
before_filter :require_allow_write_api, :only => [ :redact ]
before_filter :check_api_readable
before_filter :check_api_writable, :only => [ :redact ]
after_filter :compress_output
around_filter :api_call_handle_error, :api_call_timeout
def history
node = Node.find(params[:id])
# TODO - maybe a bit heavyweight to do this on every
# call, perhaps try lazy auth.
setup_user_auth
node = Node.find(params[:id].to_i)
doc = OSM::API.new.get_xml_doc
node.old_nodes.each do |old_node|
doc.root << old_node.to_xml_node
unless old_node.redacted? and (@user.nil? or not @user.moderator?)
doc.root << old_node.to_xml_node
end
end
render :text => doc.to_s, :content_type => "text/xml"
@ -20,14 +29,32 @@ class OldNodeController < ApplicationController
def version
if old_node = OldNode.where(:node_id => params[:id], :version => params[:version]).first
response.last_modified = old_node.timestamp
# TODO - maybe a bit heavyweight to do this on every
# call, perhaps try lazy auth.
setup_user_auth
if old_node.redacted? and (@user.nil? or not @user.moderator?)
render :nothing => true, :status => :forbidden
else
doc = OSM::API.new.get_xml_doc
doc.root << old_node.to_xml_node
render :text => doc.to_s, :content_type => "text/xml"
response.last_modified = old_node.timestamp
doc = OSM::API.new.get_xml_doc
doc.root << old_node.to_xml_node
render :text => doc.to_s, :content_type => "text/xml"
end
else
render :nothing => true, :status => :not_found
end
end
def redact
if @user && @user.moderator?
render :nothing => true
else
render :nothing => true, :status => :forbidden
end
end
end

View file

@ -3,6 +3,7 @@ class Node < ActiveRecord::Base
include GeoRecord
include ConsistencyValidations
include NotRedactable
self.table_name = "current_nodes"

View file

@ -1,6 +1,7 @@
class OldNode < ActiveRecord::Base
include GeoRecord
include ConsistencyValidations
include Redactable
self.table_name = "nodes"
self.primary_keys = "node_id", "version"
@ -12,7 +13,9 @@ class OldNode < ActiveRecord::Base
validates_associated :changeset
belongs_to :changeset
belongs_to :redaction
belongs_to :current_node, :class_name => "Node", :foreign_key => "node_id"
def validate_position
errors.add(:base, "Node is not in the world") unless in_world?
end
@ -106,4 +109,10 @@ class OldNode < ActiveRecord::Base
def containing_relation_members
return []
end
# check whether this element is the latest version - that is,
# has the same version as its "current" counterpart.
def is_latest_version?
current_node.version == self.version
end
end

View file

@ -1,10 +1,13 @@
class OldRelation < ActiveRecord::Base
include ConsistencyValidations
include Redactable
self.table_name = "relations"
self.primary_keys = "relation_id", "version"
belongs_to :changeset
belongs_to :redaction
belongs_to :current_relation, :class_name => "Relation", :foreign_key => "relation_id"
has_many :old_members, :class_name => 'OldRelationMember', :foreign_key => [:relation_id, :version], :order => :sequence_id
has_many :old_tags, :class_name => 'OldRelationTag', :foreign_key => [:relation_id, :version]
@ -130,4 +133,10 @@ class OldRelation < ActiveRecord::Base
def containing_relation_members
return []
end
# check whether this element is the latest version - that is,
# has the same version as its "current" counterpart.
def is_latest_version?
current_relation.version == self.version
end
end

View file

@ -1,10 +1,13 @@
class OldWay < ActiveRecord::Base
include ConsistencyValidations
include Redactable
self.table_name = "ways"
self.primary_keys = "way_id", "version"
belongs_to :changeset
belongs_to :redaction
belongs_to :current_way, :class_name => "Way", :foreign_key => "way_id"
has_many :old_nodes, :class_name => 'OldWayNode', :foreign_key => [:way_id, :version]
has_many :old_tags, :class_name => 'OldWayTag', :foreign_key => [:way_id, :version]
@ -158,4 +161,10 @@ class OldWay < ActiveRecord::Base
def containing_relation_members
return []
end
# check whether this element is the latest version - that is,
# has the same version as its "current" counterpart.
def is_latest_version?
current_way.version == self.version
end
end

14
app/models/redaction.rb Normal file
View file

@ -0,0 +1,14 @@
##
# Redaction represents a record associated with a particular
# action on the database to hide revisions from the history
# which are not appropriate to redistribute any more.
#
# The circumstances of the redaction can be recorded in the
# record's title and description fields, which can be
# displayed linked from the redacted records.
#
class Redaction < ActiveRecord::Base
has_many :nodes
has_many :ways
has_many :relations
end

View file

@ -2,7 +2,8 @@ class Relation < ActiveRecord::Base
require 'xml/libxml'
include ConsistencyValidations
include NotRedactable
self.table_name = "current_relations"
belongs_to :changeset

View file

@ -2,6 +2,7 @@ class Way < ActiveRecord::Base
require 'xml/libxml'
include ConsistencyValidations
include NotRedactable
self.table_name = "current_ways"

View file

@ -16,6 +16,7 @@ OpenStreetMap::Application.routes.draw do
match 'api/0.6/node/:id/ways' => 'way#ways_for_node', :via => :get, :id => /\d+/
match 'api/0.6/node/:id/relations' => 'relation#relations_for_node', :via => :get, :id => /\d+/
match 'api/0.6/node/:id/history' => 'old_node#history', :via => :get, :id => /\d+/
match 'api/0.6/node/:id/:version/redact' => 'old_node#redact', :version => /\d+/, :id => /\d+/
match 'api/0.6/node/:id/:version' => 'old_node#version', :via => :get, :id => /\d+/, :version => /\d+/
match 'api/0.6/node/:id' => 'node#read', :via => :get, :id => /\d+/
match 'api/0.6/node/:id' => 'node#update', :via => :put, :id => /\d+/

11
lib/not_redactable.rb Normal file
View file

@ -0,0 +1,11 @@
require 'osm'
module NotRedactable
def redacted?
false
end
def redact!(r)
raise OSM::APICannotRedactError.new
end
end

View file

@ -268,6 +268,19 @@ module OSM
end
end
##
# raised when someone tries to redact a current version of
# an element - only historical versions can be redacted.
class APICannotRedactError < APIError
def status
:bad_request
end
def to_s
"Cannot redact current version of element, only historical versions may be redacted."
end
end
# Helper methods for going to/from mercator and lat/lng.
class Mercator
include Math

15
lib/redactable.rb Normal file
View file

@ -0,0 +1,15 @@
require 'osm'
module Redactable
def redacted?
not self.redaction.nil?
end
def redact!(redaction)
# check that this version isn't the current version
raise OSM::APICannotRedactError.new if self.is_latest_version?
# make the change
self.redaction = redaction
end
end

View file

@ -161,3 +161,13 @@ public_visible_node:
tile: <%= QuadTile.tile_for_point(1,1) %>
timestamp: 2007-01-01 00:00:00
redacted_node:
id: 17
latitude: <%= 1*SCALE %>
longitude: <%= 1*SCALE %>
changeset_id: 2
visible: false
version: 2
tile: <%= QuadTile.tile_for_point(1,1) %>
timestamp: 2007-01-01 00:00:00

View file

@ -191,3 +191,23 @@ public_visible_node:
tile: <%= QuadTile.tile_for_point(1,1) %>
timestamp: 2007-01-01 00:00:00
redacted_node_redacted_version:
node_id: 17
latitude: <%= 1*SCALE %>
longitude: <%= 1*SCALE %>
changeset_id: 2
visible: true
version: 1
tile: <%= QuadTile.tile_for_point(1,1) %>
timestamp: 2007-01-01 00:00:00
redaction_id: 1
redacted_node_current_version:
node_id: 17
latitude: <%= 1*SCALE %>
longitude: <%= 1*SCALE %>
changeset_id: 2
visible: false
version: 2
tile: <%= QuadTile.tile_for_point(1,1) %>
timestamp: 2007-01-01 00:00:00

6
test/fixtures/redactions.yml vendored Normal file
View file

@ -0,0 +1,6 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
example:
id: 1
title: Example redaction
description: Usually some text would go here to say something about why the redaction happened.

View file

@ -164,7 +164,121 @@ class OldNodeControllerTest < ActionController::TestCase
check_current_version(current_nodes(:node_used_by_relationship))
check_current_version(current_nodes(:node_with_versions))
end
##
# test the redaction of an old version of a node, while not being
# authorised.
def test_redact_node_unauthorised
do_redact_node(nodes(:node_with_versions_v3),
redactions(:example))
assert_response :unauthorized, "should need to be authenticated to redact."
end
##
# test the redaction of an old version of a node, while being
# authorised as a normal user.
def test_redact_node_normal_user
basic_authorization(users(:public_user).email, "test")
do_redact_node(nodes(:node_with_versions_v3),
redactions(:example))
assert_response :forbidden, "should need to be moderator to redact."
end
##
# test that, even as moderator, the current version of a node
# can't be redacted.
def test_redact_node_current_version
basic_authorization(users(:moderator_user).email, "test")
do_redact_node(nodes(:node_with_versions_v4),
redactions(:example))
assert_response :forbidden, "shouldn't be OK to redact current version as moderator."
end
##
# test that redacted nodes aren't visible, regardless of
# authorisation except as moderator...
def test_version_redacted
node = nodes(:redacted_node_redacted_version)
get :version, :id => node.node_id, :version => node.version
assert_response :forbidden, "Redacted node shouldn't be visible via the version API."
# not even to a logged-in user
basic_authorization(users(:public_user).email, "test")
get :version, :id => node.node_id, :version => node.version
assert_response :forbidden, "Redacted node shouldn't be visible via the version API, even when logged in."
end
##
# test that redacted nodes aren't visible in the history
def test_history_redacted
node = nodes(:redacted_node_redacted_version)
get :history, :id => node.node_id
assert_response :success, "Redaction shouldn't have stopped history working."
assert_select "osm node[id=#{node.node_id}][version=#{node.version}]", 0, "redacted node #{node.node_id} version #{node.version} shouldn't be present in the history."
# not even to a logged-in user
basic_authorization(users(:public_user).email, "test")
get :version, :id => node.node_id, :version => node.version
get :history, :id => node.node_id
assert_response :success, "Redaction shouldn't have stopped history working."
assert_select "osm node[id=#{node.node_id}][version=#{node.version}]", 0, "redacted node #{node.node_id} version #{node.version} shouldn't be present in the history, even when logged in."
end
##
# test the redaction of an old version of a node, while being
# authorised as a moderator.
def test_redact_node_moderator
node = nodes(:node_with_versions_v3)
basic_authorization(users(:moderator_user).email, "test")
do_redact_node(node, redactions(:example))
assert_response :success, "should be OK to redact old version as moderator."
# check moderator can still see the redacted data
get :version, :id => node.node_id, :version => node.version
assert_response :success, "After redaction, node should not be gone for moderator."
# and when accessed via history
get :history, :id => node.node_id
assert_response :success, "Redaction shouldn't have stopped history working."
assert_select "osm node[id=#{node.node_id}][version=#{node.version}]", 1, "node #{node.node_id} version #{node.version} should still be present in the history for moderators."
end
# testing that if the moderator drops auth, he can't see the
# redacted stuff any more.
def test_redact_node_is_redacted
node = nodes(:node_with_versions_v3)
basic_authorization(users(:moderator_user).email, "test")
do_redact_node(node, redactions(:example))
assert_response :success, "should be OK to redact old version as moderator."
# re-auth as non-moderator
basic_authorization(users(:public_user).email, "test")
# check can't see the redacted data
get :version, :id => node.node_id, :version => node.version
assert_response :forbidden, "Redacted node shouldn't be visible via the version API."
# and when accessed via history
get :version, :id => node.node_id, :version => node.version
get :history, :id => node.node_id
assert_response :success, "Redaction shouldn't have stopped history working."
assert_select "osm node[id=#{node.node_id}][version=#{node.version}]", 0, "redacted node #{node.node_id} version #{node.version} shouldn't be present in the history."
end
def do_redact_node(node, redaction)
get :version, :id => node.node_id, :version => node.version
assert_response :success, "should be able to get version #{node.version} of node #{node.node_id}."
# now redact it
post :redact, :id => node.node_id, :version => node.version, :redaction => redaction.id
end
def check_current_version(node_id)
# get the current version of the node
current_node = with_controller(NodeController.new) do

View file

@ -51,6 +51,8 @@ class ActiveSupport::TestCase
set_fixture_class :gpx_file_tags => 'Tracetag'
fixtures :client_applications
fixtures :redactions
end
##

View file

@ -4,7 +4,7 @@ class NodeTest < ActiveSupport::TestCase
api_fixtures
def test_node_count
assert_equal 16, Node.count
assert_equal 17, Node.count
end
def test_node_too_far_north

View file

@ -4,7 +4,7 @@ class OldNodeTest < ActiveSupport::TestCase
api_fixtures
def test_old_node_count
assert_equal 19, OldNode.count
assert_equal 21, OldNode.count
end
def test_node_too_far_north

View file

@ -0,0 +1,36 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'osm'
class RedactionTest < ActiveSupport::TestCase
api_fixtures
fixtures :redactions
def test_cannot_redact_current
n = current_nodes(:node_with_versions)
r = redactions(:example)
assert_equal(false, n.redacted?, "Expected node to not be redacted already.")
assert_raise(OSM::APICannotRedactError) do
n.redact!(r)
end
end
def test_cannot_redact_current_via_old
n = nodes(:node_with_versions_v4)
r = redactions(:example)
assert_equal(false, n.redacted?, "Expected node to not be redacted already.")
assert_raise(OSM::APICannotRedactError) do
n.redact!(r)
end
end
def test_can_redact_old
n = nodes(:node_with_versions_v3)
r = redactions(:example)
assert_equal(false, n.redacted?, "Expected node to not be redacted already.")
assert_nothing_raised(OSM::APICannotRedactError) do
n.redact!(r)
end
assert_equal(true, n.redacted?, "Expected node to be redacted after redact! call.")
end
end