Merge remote-tracking branch 'upstream/pull/4319'

This commit is contained in:
Tom Hughes 2023-11-15 17:14:49 +00:00
commit cebda5ffb9
26 changed files with 815 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg6042"
version="1.1"
inkscape:version="0.48.2 r9819"
width="20"
height="20"
sodipodi:docname="blank_moderator.svg"
inkscape:export-filename="/Users/saman/work_repos/openstreetmap-website/app/assets/images/roles/blank_moderator.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata6048">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6046" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1264"
inkscape:window-height="776"
id="namedview6044"
showgrid="false"
inkscape:zoom="1"
inkscape:cx="10"
inkscape:cy="10"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg6042" />
<path
inkscape:connector-curvature="0"
style="color:#000000;fill:#38e13a;fill-opacity:1;fill-rule:nonzero;stroke:#38e13a;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 10,2 8.125,8 2,8 6.96875,11.71875 5,18 10,14 15,18 13.03125,11.71875 18,8 11.875,8 10,2 z"
id="path4709" />
<path
inkscape:connector-curvature="0"
style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="M 10,2 8.125,8 2,8 6.96875,11.71875 5,18 10,14 15,18 13.03125,11.71875 18,8 11.875,8 10,2 z"
id="path5684" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

View file

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg4678"
version="1.1"
inkscape:version="0.48.2 r9819"
width="20"
height="20"
sodipodi:docname="moderator.svg"
inkscape:export-filename="/Users/saman/work_repos/openstreetmap-website/app/assets/images/roles/moderator.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata4684">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4682" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1574"
inkscape:window-height="831"
id="namedview4680"
showgrid="false"
inkscape:zoom="1"
inkscape:cx="9.1260993"
inkscape:cy="11.531765"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg4678">
<inkscape:grid
type="xygrid"
id="grid4707" />
</sodipodi:namedview>
<path
style="color:#000000;fill:#38e13a;fill-opacity:1;fill-rule:nonzero;stroke:#38e13a;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 10,2 5,16 -5,-4 -5,4 z"
id="path4709"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="color:#000000;fill:#38e13a;fill-opacity:1;fill-rule:nonzero;stroke:#38e13a;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
d="m 2,8 16,0 -8,6 z"
id="path5479"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -92,6 +92,10 @@ module Api
diff_reader = DiffReader.new(request.raw_post, changeset)
Changeset.transaction do
result = diff_reader.commit
# the number of changes in this changeset has already been
# updated and is visible in this transaction so we don't need
# to allow for any more when checking the limit
check_rate_limit(0)
render :xml => result.to_s
end
end

View file

@ -14,6 +14,7 @@ module Api
around_action :api_call_handle_error, :api_call_timeout
before_action :set_request_formats, :except => [:create, :update, :delete]
before_action :check_rate_limit, :only => [:create, :update, :delete]
# Dump the details on many nodes whose ids are given in the "nodes" parameter.
def index

View file

@ -12,6 +12,7 @@ module Api
around_action :api_call_handle_error, :api_call_timeout
before_action :set_request_formats, :except => [:create, :update, :delete]
before_action :check_rate_limit, :only => [:create, :update, :delete]
def index
raise OSM::APIBadUserInput, "The parameter relations is required, and must be of the form relations=id[,id[,id...]]" unless params["relations"]

View file

@ -12,6 +12,7 @@ module Api
around_action :api_call_handle_error, :api_call_timeout
before_action :set_request_formats, :except => [:create, :update, :delete]
before_action :check_rate_limit, :only => [:create, :update, :delete]
def index
raise OSM::APIBadUserInput, "The parameter ways is required, and must be of the form ways=id[,id[,id...]]" unless params["ways"]

View file

@ -192,4 +192,14 @@ class ApiController < ApplicationController
ActiveRecord::Base.connection.raw_connection.cancel
raise OSM::APITimeoutError
end
##
# check the api change rate limit
def check_rate_limit(new_changes = 1)
max_changes = ActiveRecord::Base.connection.select_value(
"SELECT api_rate_limit($1)", "api_rate_limit", [current_user.id]
)
raise OSM::APIRateLimitExceeded if new_changes > max_changes
end
end

View file

@ -290,6 +290,12 @@ class User < ApplicationRecord
role? "administrator"
end
##
# returns true if the user has the importer role, false otherwise
def importer?
role? "importer"
end
##
# returns true if the user has the requested role
def role?(role)

View file

@ -23,7 +23,7 @@ class UserRole < ApplicationRecord
belongs_to :user
belongs_to :granter, :class_name => "User"
ALL_ROLES = %w[administrator moderator].freeze
ALL_ROLES = %w[administrator moderator importer].freeze
validates :role, :inclusion => ALL_ROLES, :uniqueness => { :scope => :user_id }
end

View file

@ -51,6 +51,10 @@ if defined?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
execute "DROP TYPE #{enumeration_name}"
end
def add_enumeration_value(enumeration_name, value)
execute "ALTER TYPE #{enumeration_name} ADD VALUE '#{value}'"
end
def rename_enumeration(old_name, new_name)
old_name = quote_table_name(old_name)
new_name = quote_table_name(new_name)

View file

@ -2739,12 +2739,15 @@ en:
role:
administrator: "This user is an administrator"
moderator: "This user is a moderator"
importer: "This user is a importer"
grant:
administrator: "Grant administrator access"
moderator: "Grant moderator access"
importer: "Grant importer access"
revoke:
administrator: "Revoke administrator access"
moderator: "Revoke moderator access"
importer: "Revoke importer access"
block_history: "Active Blocks"
moderator_history: "Blocks Given"
comments: "Comments"

View file

@ -62,6 +62,13 @@ min_changeset_comments_per_hour: 1
initial_changeset_comments_per_hour: 6
max_changeset_comments_per_hour: 60
moderator_changeset_comments_per_hour: 36000
# Rate limit for changes
min_changes_per_hour: 100
initial_changes_per_hour: 1000
max_changes_per_hour: 100000
days_to_max_changes: 7
importer_changes_per_hour: 1000000
moderator_changes_per_hour: 1000000
# Domain for handling message replies
#messages_domain: "messages.openstreetmap.org"
# MaxMind GeoIPv2 database

View file

@ -0,0 +1,9 @@
class AddImporterRole < ActiveRecord::Migration[7.1]
def up
add_enumeration_value :user_role_enum, "importer"
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View file

@ -0,0 +1,13 @@
class ApiRateLimit < ActiveRecord::Migration[7.1]
def up
safety_assured do
execute DatabaseFunctions::API_RATE_LIMIT
end
end
def down
safety_assured do
execute "DROP FUNCTION api_rate_limit(bigint)"
end
end
end

View file

@ -91,7 +91,8 @@ CREATE TYPE public.nwr_enum AS ENUM (
CREATE TYPE public.user_role_enum AS ENUM (
'administrator',
'moderator'
'moderator',
'importer'
);
@ -107,6 +108,67 @@ CREATE TYPE public.user_status_enum AS ENUM (
'deleted'
);
--
-- Name: api_rate_limit(bigint); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.api_rate_limit(user_id bigint) RETURNS integer
LANGUAGE plpgsql STABLE
AS $$
DECLARE
min_changes_per_hour int4 := 100;
initial_changes_per_hour int4 := 1000;
max_changes_per_hour int4 := 100000;
days_to_max_changes int4 := 7;
importer_changes_per_hour int4 := 1000000;
moderator_changes_per_hour int4 := 1000000;
roles text[];
last_block timestamp without time zone;
first_change timestamp without time zone;
active_reports int4;
time_since_first_change double precision;
max_changes double precision;
recent_changes int4;
BEGIN
SELECT ARRAY_AGG(user_roles.role) INTO STRICT roles FROM user_roles WHERE user_roles.user_id = api_rate_limit.user_id;
IF 'moderator' = ANY(roles) THEN
max_changes := moderator_changes_per_hour;
ELSIF 'importer' = ANY(roles) THEN
max_changes := importer_changes_per_hour;
ELSE
SELECT user_blocks.created_at INTO last_block FROM user_blocks WHERE user_blocks.user_id = api_rate_limit.user_id ORDER BY user_blocks.created_at DESC LIMIT 1;
IF FOUND THEN
SELECT changesets.created_at INTO first_change FROM changesets WHERE changesets.user_id = api_rate_limit.user_id AND changesets.created_at > last_block ORDER BY changesets.created_at LIMIT 1;
ELSE
SELECT changesets.created_at INTO first_change FROM changesets WHERE changesets.user_id = api_rate_limit.user_id ORDER BY changesets.created_at LIMIT 1;
END IF;
IF NOT FOUND THEN
first_change := CURRENT_TIMESTAMP AT TIME ZONE 'UTC';
END IF;
SELECT COUNT(*) INTO STRICT active_reports
FROM issues INNER JOIN reports ON reports.issue_id = issues.id
WHERE issues.reported_user_id = api_rate_limit.user_id AND issues.status = 'open' AND reports.updated_at >= COALESCE(issues.resolved_at, '1970-01-01');
time_since_first_change := EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - first_change);
max_changes := max_changes_per_hour * POWER(time_since_first_change, 2) / POWER(days_to_max_changes * 24 * 60 * 60, 2);
max_changes := GREATEST(initial_changes_per_hour, LEAST(max_changes_per_hour, FLOOR(max_changes)));
max_changes := max_changes / POWER(2, active_reports);
max_changes := GREATEST(min_changes_per_hour, LEAST(max_changes_per_hour, max_changes));
END IF;
SELECT COALESCE(SUM(changesets.num_changes), 0) INTO STRICT recent_changes FROM changesets WHERE changesets.user_id = api_rate_limit.user_id AND changesets.created_at >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - '1 hour'::interval;
RETURN max_changes - recent_changes;
END;
$$;
SET default_tablespace = '';
SET default_table_access_method = heap;
@ -3437,6 +3499,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('23'),
('22'),
('21'),
('20231101222146'),
('20231029151516'),
('20231010194809'),
('20231007141103'),
('20230830115220'),

View file

@ -1,4 +1,4 @@
FROM postgres:11
FROM postgres:14
# Add db init script to install OSM-specific Postgres user.
ADD docker/postgres/openstreetmap-postgres-init.sh /docker-entrypoint-initdb.d/

58
lib/database_functions.rb Normal file
View file

@ -0,0 +1,58 @@
module DatabaseFunctions
API_RATE_LIMIT = %(
CREATE OR REPLACE FUNCTION api_rate_limit(user_id int8)
RETURNS int4
AS $$
DECLARE
min_changes_per_hour int4 := #{Settings.min_changes_per_hour};
initial_changes_per_hour int4 := #{Settings.initial_changes_per_hour};
max_changes_per_hour int4 := #{Settings.max_changes_per_hour};
days_to_max_changes int4 := #{Settings.days_to_max_changes};
importer_changes_per_hour int4 := #{Settings.importer_changes_per_hour};
moderator_changes_per_hour int4 := #{Settings.moderator_changes_per_hour};
roles text[];
last_block timestamp without time zone;
first_change timestamp without time zone;
active_reports int4;
time_since_first_change double precision;
max_changes double precision;
recent_changes int4;
BEGIN
SELECT ARRAY_AGG(user_roles.role) INTO STRICT roles FROM user_roles WHERE user_roles.user_id = api_rate_limit.user_id;
IF 'moderator' = ANY(roles) THEN
max_changes := moderator_changes_per_hour;
ELSIF 'importer' = ANY(roles) THEN
max_changes := importer_changes_per_hour;
ELSE
SELECT user_blocks.created_at INTO last_block FROM user_blocks WHERE user_blocks.user_id = api_rate_limit.user_id ORDER BY user_blocks.created_at DESC LIMIT 1;
IF FOUND THEN
SELECT changesets.created_at INTO first_change FROM changesets WHERE changesets.user_id = api_rate_limit.user_id AND changesets.created_at > last_block ORDER BY changesets.created_at LIMIT 1;
ELSE
SELECT changesets.created_at INTO first_change FROM changesets WHERE changesets.user_id = api_rate_limit.user_id ORDER BY changesets.created_at LIMIT 1;
END IF;
IF NOT FOUND THEN
first_change := CURRENT_TIMESTAMP AT TIME ZONE 'UTC';
END IF;
SELECT COUNT(*) INTO STRICT active_reports
FROM issues INNER JOIN reports ON reports.issue_id = issues.id
WHERE issues.reported_user_id = api_rate_limit.user_id AND issues.status = 'open' AND reports.updated_at >= COALESCE(issues.resolved_at, '1970-01-01');
time_since_first_change := EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - first_change);
max_changes := max_changes_per_hour * POWER(time_since_first_change, 2) / POWER(days_to_max_changes * 24 * 60 * 60, 2);
max_changes := GREATEST(initial_changes_per_hour, LEAST(max_changes_per_hour, FLOOR(max_changes)));
max_changes := max_changes / POWER(2, active_reports);
max_changes := GREATEST(min_changes_per_hour, LEAST(max_changes_per_hour, max_changes));
END IF;
SELECT COALESCE(SUM(changesets.num_changes), 0) INTO STRICT recent_changes FROM changesets WHERE changesets.user_id = api_rate_limit.user_id AND changesets.created_at >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - '1 hour'::interval;
RETURN max_changes - recent_changes;
END;
$$ LANGUAGE plpgsql STABLE;
).freeze
end

View file

@ -0,0 +1,6 @@
namespace :db do
desc "Update database function definitions"
task :update_functions => :environment do
ActiveRecord::Base.connection.execute DatabaseFunctions::API_RATE_LIMIT
end
end

View file

@ -1606,6 +1606,107 @@ module Api
assert_equal "Precondition failed: Node #{node.id} is still used by ways #{way.id}.", @response.body
end
##
# test initial rate limit
def test_upload_initial_rate_limit
# create a user
user = create(:user)
# create some objects to use
node = create(:node)
way = create(:way_with_nodes, :nodes_count => 2)
relation = create(:relation)
# create a changeset that puts us near the initial rate limit
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => Settings.initial_changes_per_hour - 2)
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# simple diff to create a node way and relation using placeholders
diff = <<~CHANGESET
<osmChange>
<create>
<node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
<tag k='foo' v='bar'/>
<tag k='baz' v='bat'/>
</node>
<way id='-1' changeset='#{changeset.id}'>
<nd ref='#{node.id}'/>
</way>
</create>
<create>
<relation id='-1' changeset='#{changeset.id}'>
<member type='way' role='some' ref='#{way.id}'/>
<member type='node' role='some' ref='#{node.id}'/>
<member type='relation' role='some' ref='#{relation.id}'/>
</relation>
</create>
</osmChange>
CHANGESET
# upload it
post changeset_upload_path(changeset), :params => diff, :headers => auth_header
assert_response :too_many_requests, "upload did not hit rate limit"
end
##
# test maximum rate limit
def test_upload_maximum_rate_limit
# create a user
user = create(:user)
# create some objects to use
node = create(:node)
way = create(:way_with_nodes, :nodes_count => 2)
relation = create(:relation)
# create a changeset to establish our initial edit time
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 28.days)
# create changeset to put us near the maximum rate limit
total_changes = Settings.max_changes_per_hour - 2
while total_changes.positive?
changes = [total_changes, Changeset::MAX_ELEMENTS].min
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => changes)
total_changes -= changes
end
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# simple diff to create a node way and relation using placeholders
diff = <<~CHANGESET
<osmChange>
<create>
<node id='-1' lon='0' lat='0' changeset='#{changeset.id}'>
<tag k='foo' v='bar'/>
<tag k='baz' v='bat'/>
</node>
<way id='-1' changeset='#{changeset.id}'>
<nd ref='#{node.id}'/>
</way>
</create>
<create>
<relation id='-1' changeset='#{changeset.id}'>
<member type='way' role='some' ref='#{way.id}'/>
<member type='node' role='some' ref='#{node.id}'/>
<member type='relation' role='some' ref='#{relation.id}'/>
</relation>
</create>
</osmChange>
CHANGESET
# upload it
post changeset_upload_path(changeset), :params => diff, :headers => auth_header
assert_response :too_many_requests, "upload did not hit rate limit"
end
##
# when we make some simple changes we get the same changes back from the
# diff download.
@ -2183,7 +2284,11 @@ module Api
# check that a changeset can contain a certain max number of changes.
## FIXME should be changed to an integration test due to the with_controller
def test_changeset_limits
auth_header = basic_authorization_header create(:user).email, "test"
user = create(:user)
auth_header = basic_authorization_header user.email, "test"
# create an old changeset to ensure we have the maximum rate limit
create(:changeset, :user => user, :created_at => Time.now.utc - 28.days)
# open a new changeset
xml = "<osm><changeset/></osm>"

View file

@ -558,6 +558,91 @@ module Api
assert_includes apinode.tags, "\#{@user.inspect}"
end
##
# test initial rate limit
def test_initial_rate_limit
# create a user
user = create(:user)
# create a changeset that puts us near the initial rate limit
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => Settings.initial_changes_per_hour - 1)
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# try creating a node
xml = "<osm><node lat='0' lon='0' changeset='#{changeset.id}'/></osm>"
put node_create_path, :params => xml, :headers => auth_header
assert_response :success, "node create did not return success status"
# get the id of the node we created
nodeid = @response.body
# try updating the node, which should be rate limited
xml = "<osm><node id='#{nodeid}' version='1' lat='1' lon='1' changeset='#{changeset.id}'/></osm>"
put api_node_path(nodeid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "node update did not hit rate limit"
# try deleting the node, which should be rate limited
xml = "<osm><node id='#{nodeid}' version='2' lat='1' lon='1' changeset='#{changeset.id}'/></osm>"
delete api_node_path(nodeid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "node delete did not hit rate limit"
# try creating a node, which should be rate limited
xml = "<osm><node lat='0' lon='0' changeset='#{changeset.id}'/></osm>"
put node_create_path, :params => xml, :headers => auth_header
assert_response :too_many_requests, "node create did not hit rate limit"
end
##
# test maximum rate limit
def test_maximum_rate_limit
# create a user
user = create(:user)
# create a changeset to establish our initial edit time
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 28.days)
# create changeset to put us near the maximum rate limit
total_changes = Settings.max_changes_per_hour - 1
while total_changes.positive?
changes = [total_changes, Changeset::MAX_ELEMENTS].min
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => changes)
total_changes -= changes
end
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# try creating a node
xml = "<osm><node lat='0' lon='0' changeset='#{changeset.id}'/></osm>"
put node_create_path, :params => xml, :headers => auth_header
assert_response :success, "node create did not return success status"
# get the id of the node we created
nodeid = @response.body
# try updating the node, which should be rate limited
xml = "<osm><node id='#{nodeid}' version='1' lat='1' lon='1' changeset='#{changeset.id}'/></osm>"
put api_node_path(nodeid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "node update did not hit rate limit"
# try deleting the node, which should be rate limited
xml = "<osm><node id='#{nodeid}' version='2' lat='1' lon='1' changeset='#{changeset.id}'/></osm>"
delete api_node_path(nodeid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "node delete did not hit rate limit"
# try creating a node, which should be rate limited
xml = "<osm><node lat='0' lon='0' changeset='#{changeset.id}'/></osm>"
put node_create_path, :params => xml, :headers => auth_header
assert_response :too_many_requests, "node create did not hit rate limit"
end
private
##

View file

@ -906,6 +906,117 @@ module Api
end
end
##
# test initial rate limit
def test_initial_rate_limit
# create a user
user = create(:user)
# create some nodes
node1 = create(:node)
node2 = create(:node)
# create a changeset that puts us near the initial rate limit
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => Settings.initial_changes_per_hour - 1)
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# try creating a relation
xml = "<osm><relation changeset='#{changeset.id}'>" \
"<member ref='#{node1.id}' type='node' role='some'/>" \
"<member ref='#{node2.id}' type='node' role='some'/>" \
"<tag k='test' v='yes' /></relation></osm>"
put relation_create_path, :params => xml, :headers => auth_header
assert_response :success, "relation create did not return success status"
# get the id of the relation we created
relationid = @response.body
# try updating the relation, which should be rate limited
xml = "<osm><relation id='#{relationid}' version='1' changeset='#{changeset.id}'>" \
"<member ref='#{node2.id}' type='node' role='some'/>" \
"<member ref='#{node1.id}' type='node' role='some'/>" \
"<tag k='test' v='yes' /></relation></osm>"
put api_relation_path(relationid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "relation update did not hit rate limit"
# try deleting the relation, which should be rate limited
xml = "<osm><relation id='#{relationid}' version='2' changeset='#{changeset.id}'/></osm>"
delete api_relation_path(relationid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "relation delete did not hit rate limit"
# try creating a relation, which should be rate limited
xml = "<osm><relation changeset='#{changeset.id}'>" \
"<member ref='#{node1.id}' type='node' role='some'/>" \
"<member ref='#{node2.id}' type='node' role='some'/>" \
"<tag k='test' v='yes' /></relation></osm>"
put relation_create_path, :params => xml, :headers => auth_header
assert_response :too_many_requests, "relation create did not hit rate limit"
end
##
# test maximum rate limit
def test_maximum_rate_limit
# create a user
user = create(:user)
# create some nodes
node1 = create(:node)
node2 = create(:node)
# create a changeset to establish our initial edit time
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 28.days)
# create changeset to put us near the maximum rate limit
total_changes = Settings.max_changes_per_hour - 1
while total_changes.positive?
changes = [total_changes, Changeset::MAX_ELEMENTS].min
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => changes)
total_changes -= changes
end
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# try creating a relation
xml = "<osm><relation changeset='#{changeset.id}'>" \
"<member ref='#{node1.id}' type='node' role='some'/>" \
"<member ref='#{node2.id}' type='node' role='some'/>" \
"<tag k='test' v='yes' /></relation></osm>"
put relation_create_path, :params => xml, :headers => auth_header
assert_response :success, "relation create did not return success status"
# get the id of the relation we created
relationid = @response.body
# try updating the relation, which should be rate limited
xml = "<osm><relation id='#{relationid}' version='1' changeset='#{changeset.id}'>" \
"<member ref='#{node2.id}' type='node' role='some'/>" \
"<member ref='#{node1.id}' type='node' role='some'/>" \
"<tag k='test' v='yes' /></relation></osm>"
put api_relation_path(relationid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "relation update did not hit rate limit"
# try deleting the relation, which should be rate limited
xml = "<osm><relation id='#{relationid}' version='2' changeset='#{changeset.id}'/></osm>"
delete api_relation_path(relationid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "relation delete did not hit rate limit"
# try creating a relation, which should be rate limited
xml = "<osm><relation changeset='#{changeset.id}'>" \
"<member ref='#{node1.id}' type='node' role='some'/>" \
"<member ref='#{node2.id}' type='node' role='some'/>" \
"<tag k='test' v='yes' /></relation></osm>"
put relation_create_path, :params => xml, :headers => auth_header
assert_response :too_many_requests, "relation create did not hit rate limit"
end
private
def check_relations_for_element(path, type, id, expected_relations)

View file

@ -753,6 +753,111 @@ module Api
end
end
##
# test initial rate limit
def test_initial_rate_limit
# create a user
user = create(:user)
# create some nodes
node1 = create(:node)
node2 = create(:node)
# create a changeset that puts us near the initial rate limit
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => Settings.initial_changes_per_hour - 1)
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# try creating a way
xml = "<osm><way changeset='#{changeset.id}'>" \
"<nd ref='#{node1.id}'/><nd ref='#{node2.id}'/>" \
"<tag k='test' v='yes' /></way></osm>"
put way_create_path, :params => xml, :headers => auth_header
assert_response :success, "way create did not return success status"
# get the id of the way we created
wayid = @response.body
# try updating the way, which should be rate limited
xml = "<osm><way id='#{wayid}' version='1' changeset='#{changeset.id}'>" \
"<nd ref='#{node2.id}'/><nd ref='#{node1.id}'/>" \
"<tag k='test' v='yes' /></way></osm>"
put api_way_path(wayid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "way update did not hit rate limit"
# try deleting the way, which should be rate limited
xml = "<osm><way id='#{wayid}' version='2' changeset='#{changeset.id}'/></osm>"
delete api_way_path(wayid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "way delete did not hit rate limit"
# try creating a way, which should be rate limited
xml = "<osm><way changeset='#{changeset.id}'>" \
"<nd ref='#{node1.id}'/><nd ref='#{node2.id}'/>" \
"<tag k='test' v='yes' /></way></osm>"
put way_create_path, :params => xml, :headers => auth_header
assert_response :too_many_requests, "way create did not hit rate limit"
end
##
# test maximum rate limit
def test_maximum_rate_limit
# create a user
user = create(:user)
# create some nodes
node1 = create(:node)
node2 = create(:node)
# create a changeset to establish our initial edit time
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 28.days)
# create changeset to put us near the maximum rate limit
total_changes = Settings.max_changes_per_hour - 1
while total_changes.positive?
changes = [total_changes, Changeset::MAX_ELEMENTS].min
changeset = create(:changeset, :user => user,
:created_at => Time.now.utc - 5.minutes,
:num_changes => changes)
total_changes -= changes
end
# create authentication header
auth_header = basic_authorization_header user.email, "test"
# try creating a way
xml = "<osm><way changeset='#{changeset.id}'>" \
"<nd ref='#{node1.id}'/><nd ref='#{node2.id}'/>" \
"<tag k='test' v='yes' /></way></osm>"
put way_create_path, :params => xml, :headers => auth_header
assert_response :success, "way create did not return success status"
# get the id of the way we created
wayid = @response.body
# try updating the way, which should be rate limited
xml = "<osm><way id='#{wayid}' version='1' changeset='#{changeset.id}'>" \
"<nd ref='#{node2.id}'/><nd ref='#{node1.id}'/>" \
"<tag k='test' v='yes' /></way></osm>"
put api_way_path(wayid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "way update did not hit rate limit"
# try deleting the way, which should be rate limited
xml = "<osm><way id='#{wayid}' version='2' changeset='#{changeset.id}'/></osm>"
delete api_way_path(wayid), :params => xml, :headers => auth_header
assert_response :too_many_requests, "way delete did not hit rate limit"
# try creating a way, which should be rate limited
xml = "<osm><way changeset='#{changeset.id}'>" \
"<nd ref='#{node1.id}'/><nd ref='#{node2.id}'/>" \
"<tag k='test' v='yes' /></way></osm>"
put way_create_path, :params => xml, :headers => auth_header
assert_response :too_many_requests, "way create did not hit rate limit"
end
private
##

View file

@ -47,6 +47,12 @@ FactoryBot.define do
end
end
factory :importer_user do
after(:create) do |user, _evaluator|
create(:user_role, :role => "importer", :user => user)
end
end
factory :moderator_user do
after(:create) do |user, _evaluator|
create(:user_role, :role => "moderator", :user => user)

View file

@ -9,17 +9,27 @@ class UserRolesHelperTest < ActionView::TestCase
icon = role_icon(current_user, "moderator")
assert_dom_equal "", icon
icon = role_icon(current_user, "importer")
assert_dom_equal "", icon
icon = role_icon(create(:moderator_user), "moderator")
expected = <<~HTML.delete("\n")
<img srcset="/images/roles/moderator.svg" border="0" alt="This user is a moderator" title="This user is a moderator" src="/images/roles/moderator.png" width="20" height="20" />
HTML
assert_dom_equal expected, icon
icon = role_icon(create(:importer_user), "importer")
expected = <<~HTML.delete("\n")
<img srcset="/images/roles/importer.svg" border="0" alt="This user is a importer" title="This user is a importer" src="/images/roles/importer.png" width="20" height="20" />
HTML
assert_dom_equal expected, icon
end
def test_role_icon_administrator
self.current_user = create(:administrator_user)
user = create(:user)
icon = role_icon(user, "moderator")
expected = <<~HTML.delete("\n")
<a data-confirm="Are you sure you want to grant the role `moderator&#39; to the user `#{user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(user.display_name)}/role/moderator/grant">
@ -28,7 +38,16 @@ class UserRolesHelperTest < ActionView::TestCase
HTML
assert_dom_equal expected, icon
icon = role_icon(user, "importer")
expected = <<~HTML.delete("\n")
<a data-confirm="Are you sure you want to grant the role `importer&#39; to the user `#{user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(user.display_name)}/role/importer/grant">
<img srcset="/images/roles/blank_importer.svg" border="0" alt="Grant importer access" title="Grant importer access" src="/images/roles/blank_importer.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icon
moderator_user = create(:moderator_user)
icon = role_icon(moderator_user, "moderator")
expected = <<~HTML.delete("\n")
<a data-confirm="Are you sure you want to revoke the role `moderator&#39; from the user `#{moderator_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(moderator_user.display_name)}/role/moderator/revoke">
@ -36,6 +55,32 @@ class UserRolesHelperTest < ActionView::TestCase
</a>
HTML
assert_dom_equal expected, icon
icon = role_icon(user, "importer")
expected = <<~HTML.delete("\n")
<a data-confirm="Are you sure you want to grant the role `importer&#39; to the user `#{user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(user.display_name)}/role/importer/grant">
<img srcset="/images/roles/blank_importer.svg" border="0" alt="Grant importer access" title="Grant importer access" src="/images/roles/blank_importer.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icon
importer_user = create(:importer_user)
icon = role_icon(user, "moderator")
expected = <<~HTML.delete("\n")
<a data-confirm="Are you sure you want to grant the role `moderator&#39; to the user `#{user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(user.display_name)}/role/moderator/grant">
<img srcset="/images/roles/blank_moderator.svg" border="0" alt="Grant moderator access" title="Grant moderator access" src="/images/roles/blank_moderator.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icon
icon = role_icon(importer_user, "importer")
expected = <<~HTML.delete("\n")
<a data-confirm="Are you sure you want to revoke the role `importer&#39; from the user `#{importer_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(importer_user.display_name)}/role/importer/revoke">
<img srcset="/images/roles/importer.svg" border="0" alt="Revoke importer access" title="Revoke importer access" src="/images/roles/importer.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icon
end
def test_role_icons_normal
@ -50,10 +95,17 @@ class UserRolesHelperTest < ActionView::TestCase
HTML
assert_dom_equal expected, icons
icons = role_icons(create(:importer_user))
expected = <<~HTML.delete("\n")
<img srcset="/images/roles/importer.svg" border="0" alt="This user is a importer" title="This user is a importer" src="/images/roles/importer.png" width="20" height="20" />
HTML
assert_dom_equal expected, icons
icons = role_icons(create(:super_user))
expected = <<~HTML.delete("\n")
<img srcset="/images/roles/administrator.svg" border="0" alt="This user is an administrator" title="This user is an administrator" src="/images/roles/administrator.png" width="20" height="20" />
<img srcset="/images/roles/moderator.svg" border="0" alt="This user is a moderator" title="This user is a moderator" src="/images/roles/moderator.png" width="20" height="20" />
<img srcset="/images/roles/importer.svg" border="0" alt="This user is a importer" title="This user is a importer" src="/images/roles/importer.png" width="20" height="20" />
HTML
assert_dom_equal expected, icons
end
@ -70,6 +122,9 @@ class UserRolesHelperTest < ActionView::TestCase
<a data-confirm="Are you sure you want to grant the role `moderator&#39; to the user `#{user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(user.display_name)}/role/moderator/grant">
<img srcset="/images/roles/blank_moderator.svg" border="0" alt="Grant moderator access" title="Grant moderator access" src="/images/roles/blank_moderator.png" width="20" height="20" />
</a>
<a data-confirm="Are you sure you want to grant the role `importer&#39; to the user `#{user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(user.display_name)}/role/importer/grant">
<img srcset="/images/roles/blank_importer.svg" border="0" alt="Grant importer access" title="Grant importer access" src="/images/roles/blank_importer.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icons
@ -82,6 +137,24 @@ class UserRolesHelperTest < ActionView::TestCase
<a data-confirm="Are you sure you want to revoke the role `moderator&#39; from the user `#{moderator_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(moderator_user.display_name)}/role/moderator/revoke">
<img srcset="/images/roles/moderator.svg" border="0" alt="Revoke moderator access" title="Revoke moderator access" src="/images/roles/moderator.png" width="20" height="20" />
</a>
<a data-confirm="Are you sure you want to grant the role `importer&#39; to the user `#{moderator_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(moderator_user.display_name)}/role/importer/grant">
<img srcset="/images/roles/blank_importer.svg" border="0" alt="Grant importer access" title="Grant importer access" src="/images/roles/blank_importer.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icons
importer_user = create(:importer_user)
icons = role_icons(importer_user)
expected = <<~HTML.delete("\n")
<a data-confirm="Are you sure you want to grant the role `administrator&#39; to the user `#{importer_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(importer_user.display_name)}/role/administrator/grant">
<img srcset="/images/roles/blank_administrator.svg" border="0" alt="Grant administrator access" title="Grant administrator access" src="/images/roles/blank_administrator.png" width="20" height="20" />
</a>
<a data-confirm="Are you sure you want to grant the role `moderator&#39; to the user `#{importer_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(importer_user.display_name)}/role/moderator/grant">
<img srcset="/images/roles/blank_moderator.svg" border="0" alt="Grant moderator access" title="Grant moderator access" src="/images/roles/blank_moderator.png" width="20" height="20" />
</a>
<a data-confirm="Are you sure you want to revoke the role `importer&#39; from the user `#{importer_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(importer_user.display_name)}/role/importer/revoke">
<img srcset="/images/roles/importer.svg" border="0" alt="Revoke importer access" title="Revoke importer access" src="/images/roles/importer.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icons
@ -94,6 +167,9 @@ class UserRolesHelperTest < ActionView::TestCase
<a data-confirm="Are you sure you want to revoke the role `moderator&#39; from the user `#{super_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(super_user.display_name)}/role/moderator/revoke">
<img srcset="/images/roles/moderator.svg" border="0" alt="Revoke moderator access" title="Revoke moderator access" src="/images/roles/moderator.png" width="20" height="20" />
</a>
<a data-confirm="Are you sure you want to revoke the role `importer&#39; from the user `#{super_user.display_name}&#39;?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(super_user.display_name)}/role/importer/revoke">
<img srcset="/images/roles/importer.svg" border="0" alt="Revoke importer access" title="Revoke importer access" src="/images/roles/importer.png" width="20" height="20" />
</a>
HTML
assert_dom_equal expected, icons
end