Merge remote-tracking branch 'upstream/pull/4319'
This commit is contained in:
commit
cebda5ffb9
26 changed files with 815 additions and 4 deletions
BIN
app/assets/images/roles/blank_importer.png
Normal file
BIN
app/assets/images/roles/blank_importer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 645 B |
65
app/assets/images/roles/blank_importer.svg
Normal file
65
app/assets/images/roles/blank_importer.svg
Normal 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 |
BIN
app/assets/images/roles/importer.png
Normal file
BIN
app/assets/images/roles/importer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 481 B |
71
app/assets/images/roles/importer.svg
Normal file
71
app/assets/images/roles/importer.svg
Normal 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 |
|
@ -92,6 +92,10 @@ module Api
|
||||||
diff_reader = DiffReader.new(request.raw_post, changeset)
|
diff_reader = DiffReader.new(request.raw_post, changeset)
|
||||||
Changeset.transaction do
|
Changeset.transaction do
|
||||||
result = diff_reader.commit
|
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
|
render :xml => result.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@ module Api
|
||||||
around_action :api_call_handle_error, :api_call_timeout
|
around_action :api_call_handle_error, :api_call_timeout
|
||||||
|
|
||||||
before_action :set_request_formats, :except => [:create, :update, :delete]
|
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.
|
# Dump the details on many nodes whose ids are given in the "nodes" parameter.
|
||||||
def index
|
def index
|
||||||
|
|
|
@ -12,6 +12,7 @@ module Api
|
||||||
around_action :api_call_handle_error, :api_call_timeout
|
around_action :api_call_handle_error, :api_call_timeout
|
||||||
|
|
||||||
before_action :set_request_formats, :except => [:create, :update, :delete]
|
before_action :set_request_formats, :except => [:create, :update, :delete]
|
||||||
|
before_action :check_rate_limit, :only => [:create, :update, :delete]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
raise OSM::APIBadUserInput, "The parameter relations is required, and must be of the form relations=id[,id[,id...]]" unless params["relations"]
|
raise OSM::APIBadUserInput, "The parameter relations is required, and must be of the form relations=id[,id[,id...]]" unless params["relations"]
|
||||||
|
|
|
@ -12,6 +12,7 @@ module Api
|
||||||
around_action :api_call_handle_error, :api_call_timeout
|
around_action :api_call_handle_error, :api_call_timeout
|
||||||
|
|
||||||
before_action :set_request_formats, :except => [:create, :update, :delete]
|
before_action :set_request_formats, :except => [:create, :update, :delete]
|
||||||
|
before_action :check_rate_limit, :only => [:create, :update, :delete]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
raise OSM::APIBadUserInput, "The parameter ways is required, and must be of the form ways=id[,id[,id...]]" unless params["ways"]
|
raise OSM::APIBadUserInput, "The parameter ways is required, and must be of the form ways=id[,id[,id...]]" unless params["ways"]
|
||||||
|
|
|
@ -192,4 +192,14 @@ class ApiController < ApplicationController
|
||||||
ActiveRecord::Base.connection.raw_connection.cancel
|
ActiveRecord::Base.connection.raw_connection.cancel
|
||||||
raise OSM::APITimeoutError
|
raise OSM::APITimeoutError
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -290,6 +290,12 @@ class User < ApplicationRecord
|
||||||
role? "administrator"
|
role? "administrator"
|
||||||
end
|
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
|
# returns true if the user has the requested role
|
||||||
def role?(role)
|
def role?(role)
|
||||||
|
|
|
@ -23,7 +23,7 @@ class UserRole < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :granter, :class_name => "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 }
|
validates :role, :inclusion => ALL_ROLES, :uniqueness => { :scope => :user_id }
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,6 +51,10 @@ if defined?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
|
||||||
execute "DROP TYPE #{enumeration_name}"
|
execute "DROP TYPE #{enumeration_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_enumeration_value(enumeration_name, value)
|
||||||
|
execute "ALTER TYPE #{enumeration_name} ADD VALUE '#{value}'"
|
||||||
|
end
|
||||||
|
|
||||||
def rename_enumeration(old_name, new_name)
|
def rename_enumeration(old_name, new_name)
|
||||||
old_name = quote_table_name(old_name)
|
old_name = quote_table_name(old_name)
|
||||||
new_name = quote_table_name(new_name)
|
new_name = quote_table_name(new_name)
|
||||||
|
|
|
@ -2739,12 +2739,15 @@ en:
|
||||||
role:
|
role:
|
||||||
administrator: "This user is an administrator"
|
administrator: "This user is an administrator"
|
||||||
moderator: "This user is a moderator"
|
moderator: "This user is a moderator"
|
||||||
|
importer: "This user is a importer"
|
||||||
grant:
|
grant:
|
||||||
administrator: "Grant administrator access"
|
administrator: "Grant administrator access"
|
||||||
moderator: "Grant moderator access"
|
moderator: "Grant moderator access"
|
||||||
|
importer: "Grant importer access"
|
||||||
revoke:
|
revoke:
|
||||||
administrator: "Revoke administrator access"
|
administrator: "Revoke administrator access"
|
||||||
moderator: "Revoke moderator access"
|
moderator: "Revoke moderator access"
|
||||||
|
importer: "Revoke importer access"
|
||||||
block_history: "Active Blocks"
|
block_history: "Active Blocks"
|
||||||
moderator_history: "Blocks Given"
|
moderator_history: "Blocks Given"
|
||||||
comments: "Comments"
|
comments: "Comments"
|
||||||
|
|
|
@ -62,6 +62,13 @@ min_changeset_comments_per_hour: 1
|
||||||
initial_changeset_comments_per_hour: 6
|
initial_changeset_comments_per_hour: 6
|
||||||
max_changeset_comments_per_hour: 60
|
max_changeset_comments_per_hour: 60
|
||||||
moderator_changeset_comments_per_hour: 36000
|
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
|
# Domain for handling message replies
|
||||||
#messages_domain: "messages.openstreetmap.org"
|
#messages_domain: "messages.openstreetmap.org"
|
||||||
# MaxMind GeoIPv2 database
|
# MaxMind GeoIPv2 database
|
||||||
|
|
9
db/migrate/20231029151516_add_importer_role.rb
Normal file
9
db/migrate/20231029151516_add_importer_role.rb
Normal 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
|
13
db/migrate/20231101222146_api_rate_limit.rb
Normal file
13
db/migrate/20231101222146_api_rate_limit.rb
Normal 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
|
|
@ -91,7 +91,8 @@ CREATE TYPE public.nwr_enum AS ENUM (
|
||||||
|
|
||||||
CREATE TYPE public.user_role_enum AS ENUM (
|
CREATE TYPE public.user_role_enum AS ENUM (
|
||||||
'administrator',
|
'administrator',
|
||||||
'moderator'
|
'moderator',
|
||||||
|
'importer'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,6 +108,67 @@ CREATE TYPE public.user_status_enum AS ENUM (
|
||||||
'deleted'
|
'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_tablespace = '';
|
||||||
|
|
||||||
SET default_table_access_method = heap;
|
SET default_table_access_method = heap;
|
||||||
|
@ -3437,6 +3499,8 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||||
('23'),
|
('23'),
|
||||||
('22'),
|
('22'),
|
||||||
('21'),
|
('21'),
|
||||||
|
('20231101222146'),
|
||||||
|
('20231029151516'),
|
||||||
('20231010194809'),
|
('20231010194809'),
|
||||||
('20231007141103'),
|
('20231007141103'),
|
||||||
('20230830115220'),
|
('20230830115220'),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM postgres:11
|
FROM postgres:14
|
||||||
|
|
||||||
# Add db init script to install OSM-specific Postgres user.
|
# Add db init script to install OSM-specific Postgres user.
|
||||||
ADD docker/postgres/openstreetmap-postgres-init.sh /docker-entrypoint-initdb.d/
|
ADD docker/postgres/openstreetmap-postgres-init.sh /docker-entrypoint-initdb.d/
|
||||||
|
|
58
lib/database_functions.rb
Normal file
58
lib/database_functions.rb
Normal 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
|
6
lib/tasks/update_functions.rake
Normal file
6
lib/tasks/update_functions.rake
Normal 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
|
|
@ -1606,6 +1606,107 @@ module Api
|
||||||
assert_equal "Precondition failed: Node #{node.id} is still used by ways #{way.id}.", @response.body
|
assert_equal "Precondition failed: Node #{node.id} is still used by ways #{way.id}.", @response.body
|
||||||
end
|
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
|
# when we make some simple changes we get the same changes back from the
|
||||||
# diff download.
|
# diff download.
|
||||||
|
@ -2183,7 +2284,11 @@ module Api
|
||||||
# check that a changeset can contain a certain max number of changes.
|
# 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
|
## FIXME should be changed to an integration test due to the with_controller
|
||||||
def test_changeset_limits
|
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
|
# open a new changeset
|
||||||
xml = "<osm><changeset/></osm>"
|
xml = "<osm><changeset/></osm>"
|
||||||
|
|
|
@ -558,6 +558,91 @@ module Api
|
||||||
assert_includes apinode.tags, "\#{@user.inspect}"
|
assert_includes apinode.tags, "\#{@user.inspect}"
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -906,6 +906,117 @@ module Api
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def check_relations_for_element(path, type, id, expected_relations)
|
def check_relations_for_element(path, type, id, expected_relations)
|
||||||
|
|
|
@ -753,6 +753,111 @@ module Api
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -47,6 +47,12 @@ FactoryBot.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :importer_user do
|
||||||
|
after(:create) do |user, _evaluator|
|
||||||
|
create(:user_role, :role => "importer", :user => user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
factory :moderator_user do
|
factory :moderator_user do
|
||||||
after(:create) do |user, _evaluator|
|
after(:create) do |user, _evaluator|
|
||||||
create(:user_role, :role => "moderator", :user => user)
|
create(:user_role, :role => "moderator", :user => user)
|
||||||
|
|
|
@ -9,17 +9,27 @@ class UserRolesHelperTest < ActionView::TestCase
|
||||||
icon = role_icon(current_user, "moderator")
|
icon = role_icon(current_user, "moderator")
|
||||||
assert_dom_equal "", icon
|
assert_dom_equal "", icon
|
||||||
|
|
||||||
|
icon = role_icon(current_user, "importer")
|
||||||
|
assert_dom_equal "", icon
|
||||||
|
|
||||||
icon = role_icon(create(:moderator_user), "moderator")
|
icon = role_icon(create(:moderator_user), "moderator")
|
||||||
expected = <<~HTML.delete("\n")
|
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" />
|
<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
|
HTML
|
||||||
assert_dom_equal expected, icon
|
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
|
end
|
||||||
|
|
||||||
def test_role_icon_administrator
|
def test_role_icon_administrator
|
||||||
self.current_user = create(:administrator_user)
|
self.current_user = create(:administrator_user)
|
||||||
|
|
||||||
user = create(:user)
|
user = create(:user)
|
||||||
|
|
||||||
icon = role_icon(user, "moderator")
|
icon = role_icon(user, "moderator")
|
||||||
expected = <<~HTML.delete("\n")
|
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">
|
<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">
|
||||||
|
@ -28,7 +38,16 @@ class UserRolesHelperTest < ActionView::TestCase
|
||||||
HTML
|
HTML
|
||||||
assert_dom_equal expected, icon
|
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)
|
moderator_user = create(:moderator_user)
|
||||||
|
|
||||||
icon = role_icon(moderator_user, "moderator")
|
icon = role_icon(moderator_user, "moderator")
|
||||||
expected = <<~HTML.delete("\n")
|
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 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">
|
||||||
|
@ -36,6 +55,32 @@ class UserRolesHelperTest < ActionView::TestCase
|
||||||
</a>
|
</a>
|
||||||
HTML
|
HTML
|
||||||
assert_dom_equal expected, icon
|
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
|
end
|
||||||
|
|
||||||
def test_role_icons_normal
|
def test_role_icons_normal
|
||||||
|
@ -50,10 +95,17 @@ class UserRolesHelperTest < ActionView::TestCase
|
||||||
HTML
|
HTML
|
||||||
assert_dom_equal expected, icons
|
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))
|
icons = role_icons(create(:super_user))
|
||||||
expected = <<~HTML.delete("\n")
|
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/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/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
|
HTML
|
||||||
assert_dom_equal expected, icons
|
assert_dom_equal expected, icons
|
||||||
end
|
end
|
||||||
|
@ -70,6 +122,9 @@ class UserRolesHelperTest < ActionView::TestCase
|
||||||
<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">
|
<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" />
|
<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>
|
||||||
|
<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
|
HTML
|
||||||
assert_dom_equal expected, icons
|
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' 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 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" />
|
<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>
|
||||||
|
<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
|
HTML
|
||||||
assert_dom_equal expected, icons
|
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' from the user `#{super_user.display_name}'?" rel="nofollow" data-method="post" href="/user/#{ERB::Util.u(super_user.display_name)}/role/moderator/revoke">
|
<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" />
|
<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>
|
||||||
|
<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
|
HTML
|
||||||
assert_dom_equal expected, icons
|
assert_dom_equal expected, icons
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue