--- /dev/null
+<?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>
--- /dev/null
+<?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>
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
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
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"]
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"]
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
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)
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
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)
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"
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
--- /dev/null
+class AddImporterRole < ActiveRecord::Migration[7.1]
+ def up
+ add_enumeration_value :user_role_enum, "importer"
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
--- /dev/null
+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
CREATE TYPE public.user_role_enum AS ENUM (
'administrator',
- 'moderator'
+ 'moderator',
+ 'importer'
);
'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;
('23'),
('22'),
('21'),
+('20231101222146'),
+('20231029151516'),
('20231010194809'),
('20231007141103'),
('20230830115220'),
-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/
--- /dev/null
+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
--- /dev/null
+namespace :db do
+ desc "Update database function definitions"
+ task :update_functions => :environment do
+ ActiveRecord::Base.connection.execute DatabaseFunctions::API_RATE_LIMIT
+ end
+end
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.
# 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>"
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
##
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)
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
##
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)
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' to the user `#{user.display_name}'?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(user.display_name)}/role/moderator/grant">
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' to the user `#{user.display_name}'?" 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' from the user `#{moderator_user.display_name}'?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(moderator_user.display_name)}/role/moderator/revoke">
</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' to the user `#{user.display_name}'?" 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' to the user `#{user.display_name}'?" 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' from the user `#{importer_user.display_name}'?" 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
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
<a data-confirm="Are you sure you want to grant the role `moderator' to the user `#{user.display_name}'?" 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' to the user `#{user.display_name}'?" 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
<a data-confirm="Are you sure you want to revoke the role `moderator' from the user `#{moderator_user.display_name}'?" 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' to the user `#{moderator_user.display_name}'?" 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' to the user `#{importer_user.display_name}'?" 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' to the user `#{importer_user.display_name}'?" 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' from the user `#{importer_user.display_name}'?" 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
<a data-confirm="Are you sure you want to revoke the role `moderator' from the user `#{super_user.display_name}'?" 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' from the user `#{super_user.display_name}'?" 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