From: Tom Hughes Date: Wed, 15 Nov 2023 22:30:41 +0000 (+0000) Subject: Merge remote-tracking branch 'upstream/pull/4349' X-Git-Tag: live~937 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/2fcee9625dcd192d0c524f27d9cb182c883e31b4?hp=dbe84a97bf48789e0f32a185f701840fc68063bd Merge remote-tracking branch 'upstream/pull/4349' --- diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index db94a610b..6f25cfeb3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -81,7 +81,7 @@ Metrics/ParameterLists: # Offense count: 56 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 27 + Max: 29 # Offense count: 2394 # This cop supports safe autocorrection (--autocorrect). diff --git a/app/assets/images/roles/blank_importer.png b/app/assets/images/roles/blank_importer.png new file mode 100644 index 000000000..1eb81180e Binary files /dev/null and b/app/assets/images/roles/blank_importer.png differ diff --git a/app/assets/images/roles/blank_importer.svg b/app/assets/images/roles/blank_importer.svg new file mode 100644 index 000000000..d4e53ec72 --- /dev/null +++ b/app/assets/images/roles/blank_importer.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/app/assets/images/roles/importer.png b/app/assets/images/roles/importer.png new file mode 100644 index 000000000..671172947 Binary files /dev/null and b/app/assets/images/roles/importer.png differ diff --git a/app/assets/images/roles/importer.svg b/app/assets/images/roles/importer.svg new file mode 100644 index 000000000..449787ad8 --- /dev/null +++ b/app/assets/images/roles/importer.svg @@ -0,0 +1,71 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 5bfe86b86..af67244bc 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -124,6 +124,7 @@ $(document).ready(function () { I18n.fallbacks = true; OSM.preferred_editor = application_data.preferredEditor; + OSM.preferred_languages = application_data.preferredLanguages; if (application_data.user) { OSM.user = application_data.user; diff --git a/app/assets/javascripts/index/query.js b/app/assets/javascripts/index/query.js index e44db9fdf..59c3a8b49 100644 --- a/app/assets/javascripts/index/query.js +++ b/app/assets/javascripts/index/query.js @@ -125,7 +125,7 @@ OSM.Query = function (map) { function featureName(feature) { var tags = feature.tags, - locales = I18n.locales.get(); + locales = OSM.preferred_languages; for (var i = 0; i < locales.length; i++) { if (tags["name:" + locales[i]]) { diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 995c43f59..1f5d0398e 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -58,10 +58,6 @@ time[title] { margin-right: $lineheight * 0.25; } -[dir=rtl] { /* no-r2 */ text-align: right; } - -[dir=ltr] { /* no-r2 */ text-align: left; } - /* Rules for icons */ .icon { diff --git a/app/controllers/api/changesets_controller.rb b/app/controllers/api/changesets_controller.rb index 7bb7a5a4d..9bdf0f2bd 100644 --- a/app/controllers/api/changesets_controller.rb +++ b/app/controllers/api/changesets_controller.rb @@ -92,6 +92,10 @@ module Api diff_reader = DiffReader.new(request.raw_post, changeset) Changeset.transaction do result = diff_reader.commit + # the number of changes in this changeset has already been + # updated and is visible in this transaction so we don't need + # to allow for any more when checking the limit + check_rate_limit(0) render :xml => result.to_s end end diff --git a/app/controllers/api/nodes_controller.rb b/app/controllers/api/nodes_controller.rb index 6934a13c0..fb808828c 100644 --- a/app/controllers/api/nodes_controller.rb +++ b/app/controllers/api/nodes_controller.rb @@ -14,6 +14,7 @@ module Api around_action :api_call_handle_error, :api_call_timeout before_action :set_request_formats, :except => [:create, :update, :delete] + before_action :check_rate_limit, :only => [:create, :update, :delete] # Dump the details on many nodes whose ids are given in the "nodes" parameter. def index diff --git a/app/controllers/api/relations_controller.rb b/app/controllers/api/relations_controller.rb index 19aed6a85..e833ae830 100644 --- a/app/controllers/api/relations_controller.rb +++ b/app/controllers/api/relations_controller.rb @@ -12,6 +12,7 @@ module Api around_action :api_call_handle_error, :api_call_timeout before_action :set_request_formats, :except => [:create, :update, :delete] + before_action :check_rate_limit, :only => [:create, :update, :delete] def index raise OSM::APIBadUserInput, "The parameter relations is required, and must be of the form relations=id[,id[,id...]]" unless params["relations"] diff --git a/app/controllers/api/ways_controller.rb b/app/controllers/api/ways_controller.rb index c4fce48d0..5e72cfe20 100644 --- a/app/controllers/api/ways_controller.rb +++ b/app/controllers/api/ways_controller.rb @@ -12,6 +12,7 @@ module Api around_action :api_call_handle_error, :api_call_timeout before_action :set_request_formats, :except => [:create, :update, :delete] + before_action :check_rate_limit, :only => [:create, :update, :delete] def index raise OSM::APIBadUserInput, "The parameter ways is required, and must be of the form ways=id[,id[,id...]]" unless params["ways"] diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 89388c0bb..7e1b06a8d 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -192,4 +192,14 @@ class ApiController < ApplicationController ActiveRecord::Base.connection.raw_connection.cancel raise OSM::APITimeoutError end + + ## + # check the api change rate limit + def check_rate_limit(new_changes = 1) + max_changes = ActiveRecord::Base.connection.select_value( + "SELECT api_rate_limit($1)", "api_rate_limit", [current_user.id] + ) + + raise OSM::APIRateLimitExceeded if new_changes > max_changes + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 5ba1b702b..36c9f4e22 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -104,11 +104,11 @@ class UsersController < ApplicationController render :action => "new" elsif current_user.auth_provider.present? # Verify external authenticator before moving on - session[:new_user] = current_user + session[:new_user] = current_user.attributes.slice("email", "display_name", "pass_crypt") redirect_to auth_url(current_user.auth_provider, current_user.auth_uid), :status => :temporary_redirect else # Save the user record - session[:new_user] = current_user + session[:new_user] = current_user.attributes.slice("email", "display_name", "pass_crypt") redirect_to :action => :terms end end @@ -170,7 +170,10 @@ class UsersController < ApplicationController redirect_to referer || edit_account_path else - self.current_user = session.delete(:new_user) + new_user = session.delete(:new_user) + verified_email = new_user.delete("verified_email") + + self.current_user = User.new(new_user) if check_signup_allowed(current_user.email) current_user.data_public = true @@ -184,6 +187,8 @@ class UsersController < ApplicationController if current_user.auth_uid.blank? current_user.auth_provider = nil current_user.auth_uid = nil + elsif current_user.email == verified_email + current_user.activate end if current_user.save @@ -272,10 +277,9 @@ class UsersController < ApplicationController redirect_to edit_account_path elsif session[:new_user] - session[:new_user].auth_provider = provider - session[:new_user].auth_uid = uid - - session[:new_user].activate if email_verified && email == session[:new_user].email + session[:new_user]["auth_provider"] = provider + session[:new_user]["auth_uid"] = uid + session[:new_user]["verified_email"] = email if email_verified redirect_to :action => "terms" else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4932bcc5e..17c36bdfe 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -48,7 +48,8 @@ module ApplicationHelper def application_data data = { :locale => I18n.locale, - :preferred_editor => preferred_editor + :preferred_editor => preferred_editor, + :preferred_languages => preferred_languages.expand.map(&:to_s) } if current_user diff --git a/app/models/user.rb b/app/models/user.rb index f804f4666..3d74b3933 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -290,6 +290,12 @@ class User < ApplicationRecord role? "administrator" end + ## + # returns true if the user has the importer role, false otherwise + def importer? + role? "importer" + end + ## # returns true if the user has the requested role def role?(role) diff --git a/app/models/user_role.rb b/app/models/user_role.rb index a081361a7..332848e42 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -23,7 +23,7 @@ class UserRole < ApplicationRecord belongs_to :user belongs_to :granter, :class_name => "User" - ALL_ROLES = %w[administrator moderator].freeze + ALL_ROLES = %w[administrator moderator importer].freeze validates :role, :inclusion => ALL_ROLES, :uniqueness => { :scope => :user_id } end diff --git a/app/views/browse/_tag.html.erb b/app/views/browse/_tag.html.erb index dceb57e7d..c0cdb5f9a 100644 --- a/app/views/browse/_tag.html.erb +++ b/app/views/browse/_tag.html.erb @@ -1,4 +1,4 @@ - <%= format_key(tag[0]) %> - <%= format_value(tag[0], tag[1]) %> + <%= format_key(tag[0]) %> + <%= format_value(tag[0], tag[1]) %> diff --git a/config/application.rb b/config/application.rb index 23f70fc20..e568c8540 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ Bundler.require(*Rails.groups) module OpenStreetMap class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 + config.load_defaults 7.1 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. @@ -29,9 +29,6 @@ module OpenStreetMap # like if you have constraints or database-specific column types config.active_record.schema_format = :sql unless Settings.status == "database_offline" - # Use rails 7.1 cache format - config.active_support.cache_format_version = 7.1 - # Use memcached for caching if required config.cache_store = :mem_cache_store, Settings.memcache_servers, { :namespace => "rails:cache" } if Settings.key?(:memcache_servers) diff --git a/config/initializers/migrate.rb b/config/initializers/migrate.rb index 0667e3346..1af67dd10 100644 --- a/config/initializers/migrate.rb +++ b/config/initializers/migrate.rb @@ -42,21 +42,6 @@ if defined?(ActiveRecord::ConnectionAdapters::AbstractAdapter) execute "ALTER TABLE #{table_name} ADD PRIMARY KEY USING INDEX #{constraint_name}" end - - def create_enumeration(enumeration_name, values) - execute "CREATE TYPE #{enumeration_name} AS ENUM ('#{values.join '\',\''}')" - end - - def drop_enumeration(enumeration_name) - execute "DROP TYPE #{enumeration_name}" - end - - def rename_enumeration(old_name, new_name) - old_name = quote_table_name(old_name) - new_name = quote_table_name(new_name) - - execute "ALTER TYPE #{old_name} RENAME TO #{new_name}" - end end end end diff --git a/config/initializers/new_framework_defaults_7_1.rb b/config/initializers/new_framework_defaults_7_1.rb deleted file mode 100644 index 6837ef861..000000000 --- a/config/initializers/new_framework_defaults_7_1.rb +++ /dev/null @@ -1,223 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 7.1 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `7.1`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -# No longer add autoloaded paths into `$LOAD_PATH`. This means that you won't be able -# to manually require files that are managed by the autoloader, which you shouldn't do anyway. -# This will reduce the size of the load path, making `require` faster if you don't use bootsnap, or reduce the size -# of the bootsnap cache if you use it. -# Rails.application.config.add_autoload_paths_to_load_path = false - -# Remove the default X-Download-Options headers since it is used only by Internet Explorer. -# If you need to support Internet Explorer, add back `"X-Download-Options" => "noopen"`. -# Rails.application.config.action_dispatch.default_headers = { -# "X-Frame-Options" => "SAMEORIGIN", -# "X-XSS-Protection" => "0", -# "X-Content-Type-Options" => "nosniff", -# "X-Permitted-Cross-Domain-Policies" => "none", -# "Referrer-Policy" => "strict-origin-when-cross-origin" -# } - -# Do not treat an `ActionController::Parameters` instance -# as equal to an equivalent `Hash` by default. -# Rails.application.config.action_controller.allow_deprecated_parameters_hash_equality = false - -# Active Record Encryption now uses SHA-256 as its hash digest algorithm. Important: If you have -# data encrypted with previous Rails versions, there are two scenarios to consider: -# -# 1. If you have +config.active_support.key_generator_hash_digest_class+ configured as SHA1 (the default -# before Rails 7.0), you need to configure SHA-1 for Active Record Encryption too: -# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1 -# 2. If you have +config.active_support.key_generator_hash_digest_class+ configured as SHA256 (the new default -# in 7.0), then you need to configure SHA-256 for Active Record Encryption: -# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256 -# -# If you don't currently have data encrypted with Active Record encryption, you can disable this setting to -# configure the default behavior starting 7.1+: -Rails.application.config.active_record.encryption.support_sha1_for_non_deterministic_encryption = false - -# No longer run after_commit callbacks on the first of multiple Active Record -# instances to save changes to the same database row within a transaction. -# Instead, run these callbacks on the instance most likely to have internal -# state which matches what was committed to the database, typically the last -# instance to save. -Rails.application.config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = false - -# Configures SQLite with a strict strings mode, which disables double-quoted string literals. -# -# SQLite has some quirks around double-quoted string literals. -# It first tries to consider double-quoted strings as identifier names, but if they don't exist -# it then considers them as string literals. Because of this, typos can silently go unnoticed. -# For example, it is possible to create an index for a non existing column. -# See https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted for more details. -Rails.application.config.active_record.sqlite3_adapter_strict_strings_by_default = true - -# Disable deprecated singular associations names -Rails.application.config.active_record.allow_deprecated_singular_associations_name = false - -# Enable the Active Job `BigDecimal` argument serializer, which guarantees -# roundtripping. Without this serializer, some queue adapters may serialize -# `BigDecimal` arguments as simple (non-roundtrippable) strings. -# -# When deploying an application with multiple replicas, old (pre-Rails 7.1) -# replicas will not be able to deserialize `BigDecimal` arguments from this -# serializer. Therefore, this setting should only be enabled after all replicas -# have been successfully upgraded to Rails 7.1. -Rails.application.config.active_job.use_big_decimal_serializer = true - -# Specify if an `ArgumentError` should be raised if `Rails.cache` `fetch` or -# `write` are given an invalid `expires_at` or `expires_in` time. -# Options are `true`, and `false`. If `false`, the exception will be reported -# as `handled` and logged instead. -Rails.application.config.active_support.raise_on_invalid_cache_expiration_time = true - -# Specify whether Query Logs will format tags using the SQLCommenter format -# (https://open-telemetry.github.io/opentelemetry-sqlcommenter/), or using the legacy format. -# Options are `:legacy` and `:sqlcommenter`. -Rails.application.config.active_record.query_log_tags_format = :sqlcommenter - -# Specify the default serializer used by `MessageEncryptor` and `MessageVerifier` -# instances. -# -# The legacy default is `:marshal`, which is a potential vector for -# deserialization attacks in cases where a message signing secret has been -# leaked. -# -# In Rails 7.1, the new default is `:json_allow_marshal` which serializes and -# deserializes with `ActiveSupport::JSON`, but can fall back to deserializing -# with `Marshal` so that legacy messages can still be read. -# -# In Rails 7.2, the default will become `:json` which serializes and -# deserializes with `ActiveSupport::JSON` only. -# -# Alternatively, you can choose `:message_pack` or `:message_pack_allow_marshal`, -# which serialize with `ActiveSupport::MessagePack`. `ActiveSupport::MessagePack` -# can roundtrip some Ruby types that are not supported by JSON, and may provide -# improved performance, but it requires the `msgpack` gem. -# -# For more information, see -# https://guides.rubyonrails.org/v7.1/configuring.html#config-active-support-message-serializer -# -# If you are performing a rolling deploy of a Rails 7.1 upgrade, wherein servers -# that have not yet been upgraded must be able to read messages from upgraded -# servers, first deploy without changing the serializer, then set the serializer -# in a subsequent deploy. -Rails.application.config.active_support.message_serializer = :json_allow_marshal - -# Enable a performance optimization that serializes message data and metadata -# together. This changes the message format, so messages serialized this way -# cannot be read by older versions of Rails. However, messages that use the old -# format can still be read, regardless of whether this optimization is enabled. -# -# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have -# not yet been upgraded must be able to read messages from upgraded servers, -# leave this optimization off on the first deploy, then enable it on a -# subsequent deploy. -Rails.application.config.active_support.use_message_serializer_for_metadata = true - -# Set the maximum size for Rails log files. -# -# `config.load_defaults 7.1` does not set this value for environments other than -# development and test. -# -# if Rails.env.local? -# Rails.application.config.log_file_size = 100 * 1024 * 1024 -# end - -# Enable raising on assignment to attr_readonly attributes. The previous -# behavior would allow assignment but silently not persist changes to the -# database. -Rails.application.config.active_record.raise_on_assign_to_attr_readonly = true - -# Enable validating only parent-related columns for presence when the parent is mandatory. -# The previous behavior was to validate the presence of the parent record, which performed an extra query -# to get the parent every time the child record was updated, even when parent has not changed. -# Rails.application.config.active_record.belongs_to_required_validates_foreign_key = false - -# Enable precompilation of `config.filter_parameters`. Precompilation can -# improve filtering performance, depending on the quantity and types of filters. -Rails.application.config.precompile_filter_parameters = true - -# Enable before_committed! callbacks on all enrolled records in a transaction. -# The previous behavior was to only run the callbacks on the first copy of a record -# if there were multiple copies of the same record enrolled in the transaction. -Rails.application.config.active_record.before_committed_on_all_records = true - -# Disable automatic column serialization into YAML. -# To keep the historic behavior, you can set it to `YAML`, however it is -# recommended to explicitly define the serialization method for each column -# rather than to rely on a global default. -Rails.application.config.active_record.default_column_serializer = nil - -# Enable a performance optimization that serializes Active Record models -# in a faster and more compact way. -# -# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have -# not yet been upgraded must be able to read caches from upgraded servers, -# leave this optimization off on the first deploy, then enable it on a -# subsequent deploy. -Rails.application.config.active_record.marshalling_format_version = 7.1 - -# Run `after_commit` and `after_*_commit` callbacks in the order they are defined in a model. -# This matches the behaviour of all other callbacks. -# In previous versions of Rails, they ran in the inverse order. -Rails.application.config.active_record.run_after_transaction_callbacks_in_order_defined = true - -# Whether a `transaction` block is committed or rolled back when exited via `return`, `break` or `throw`. -# -# Rails.application.config.active_record.commit_transaction_on_non_local_return = true - -# Controls when to generate a value for has_secure_token declarations. -# -Rails.application.config.active_record.generate_secure_token_on = :initialize - -# ** Please read carefully, this must be configured in config/application.rb ** -# Change the format of the cache entry. -# Changing this default means that all new cache entries added to the cache -# will have a different format that is not supported by Rails 7.0 -# applications. -# Only change this value after your application is fully deployed to Rails 7.1 -# and you have no plans to rollback. -# When you're ready to change format, add this to `config/application.rb` (NOT -# this file): -# config.active_support.cache_format_version = 7.1 - -# Configure Action View to use HTML5 standards-compliant sanitizers when they are supported on your -# platform. -# -# `Rails::HTML::Sanitizer.best_supported_vendor` will cause Action View to use HTML5-compliant -# sanitizers if they are supported, else fall back to HTML4 sanitizers. -# -# In previous versions of Rails, Action View always used `Rails::HTML4::Sanitizer` as its vendor. -# -# Rails.application.config.action_view.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor - -# Configure Action Text to use an HTML5 standards-compliant sanitizer when it is supported on your -# platform. -# -# `Rails::HTML::Sanitizer.best_supported_vendor` will cause Action Text to use HTML5-compliant -# sanitizers if they are supported, else fall back to HTML4 sanitizers. -# -# In previous versions of Rails, Action Text always used `Rails::HTML4::Sanitizer` as its vendor. -# -# Rails.application.config.action_text.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor - -# Configure the log level used by the DebugExceptions middleware when logging -# uncaught exceptions during requests -# Rails.application.config.action_dispatch.debug_exception_log_level = :error - -# Configure the test helpers in Action View, Action Dispatch, and rails-dom-testing to use HTML5 -# parsers. -# -# Nokogiri::HTML5 isn't supported on JRuby, so JRuby applications must set this to :html4. -# -# In previous versions of Rails, these test helpers always used an HTML4 parser. -# -# Rails.application.config.dom_testing_default_html_version = :html5 diff --git a/config/locales/en.yml b/config/locales/en.yml index 079f525a0..1a41dcce8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2739,12 +2739,15 @@ en: role: administrator: "This user is an administrator" moderator: "This user is a moderator" + importer: "This user is a importer" grant: administrator: "Grant administrator access" moderator: "Grant moderator access" + importer: "Grant importer access" revoke: administrator: "Revoke administrator access" moderator: "Revoke moderator access" + importer: "Revoke importer access" block_history: "Active Blocks" moderator_history: "Blocks Given" comments: "Comments" diff --git a/config/settings.yml b/config/settings.yml index cffd3bd31..87c467c88 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -62,6 +62,13 @@ min_changeset_comments_per_hour: 1 initial_changeset_comments_per_hour: 6 max_changeset_comments_per_hour: 60 moderator_changeset_comments_per_hour: 36000 +# Rate limit for changes +min_changes_per_hour: 100 +initial_changes_per_hour: 1000 +max_changes_per_hour: 100000 +days_to_max_changes: 7 +importer_changes_per_hour: 1000000 +moderator_changes_per_hour: 1000000 # Domain for handling message replies #messages_domain: "messages.openstreetmap.org" # MaxMind GeoIPv2 database diff --git a/db/migrate/007_add_relations.rb b/db/migrate/007_add_relations.rb index 1e5bc5d3a..29ba7ee38 100644 --- a/db/migrate/007_add_relations.rb +++ b/db/migrate/007_add_relations.rb @@ -1,7 +1,7 @@ class AddRelations < ActiveRecord::Migration[4.2] def self.up # enums work like strings but are more efficient - create_enumeration :nwr_enum, %w[Node Way Relation] + create_enum :nwr_enum, %w[Node Way Relation] # a relation can have members much like a way can have nodes. # differences: diff --git a/db/migrate/039_add_more_controls_to_gpx_files.rb b/db/migrate/039_add_more_controls_to_gpx_files.rb index 728b56989..9f6d62148 100644 --- a/db/migrate/039_add_more_controls_to_gpx_files.rb +++ b/db/migrate/039_add_more_controls_to_gpx_files.rb @@ -4,7 +4,7 @@ class AddMoreControlsToGpxFiles < ActiveRecord::Migration[4.2] end def self.up - create_enumeration :gpx_visibility_enum, %w[private public trackable identifiable] + create_enum :gpx_visibility_enum, %w[private public trackable identifiable] add_column :gpx_files, :visibility, :gpx_visibility_enum, :default => "public", :null => false Trace.where(:public => false).update_all(:visibility => "private") add_index :gpx_files, [:visible, :visibility], :name => "gpx_files_visible_visibility_idx" diff --git a/db/migrate/044_create_user_roles.rb b/db/migrate/044_create_user_roles.rb index ece98a54d..ca00d15d4 100644 --- a/db/migrate/044_create_user_roles.rb +++ b/db/migrate/044_create_user_roles.rb @@ -6,7 +6,7 @@ class CreateUserRoles < ActiveRecord::Migration[4.2] end def self.up - create_enumeration :user_role_enum, %w[administrator moderator] + create_enum :user_role_enum, %w[administrator moderator] create_table :user_roles do |t| t.column :user_id, :bigint, :null => false diff --git a/db/migrate/051_add_status_to_user.rb b/db/migrate/051_add_status_to_user.rb index f170e0de8..bea9f125f 100644 --- a/db/migrate/051_add_status_to_user.rb +++ b/db/migrate/051_add_status_to_user.rb @@ -3,7 +3,7 @@ class AddStatusToUser < ActiveRecord::Migration[4.2] end def self.up - create_enumeration :user_status_enum, %w[pending active confirmed suspended deleted] + create_enum :user_status_enum, %w[pending active confirmed suspended deleted] add_column :users, :status, :user_status_enum, :null => false, :default => "pending" diff --git a/db/migrate/053_add_map_bug_tables.rb b/db/migrate/053_add_map_bug_tables.rb index 6ad3af0eb..682a736d0 100644 --- a/db/migrate/053_add_map_bug_tables.rb +++ b/db/migrate/053_add_map_bug_tables.rb @@ -1,6 +1,6 @@ class AddMapBugTables < ActiveRecord::Migration[4.2] def self.up - create_enumeration :map_bug_status_enum, %w[open closed hidden] + create_enum :map_bug_status_enum, %w[open closed hidden] create_table :map_bugs do |t| t.integer :latitude, :null => false diff --git a/db/migrate/057_add_map_bug_comment_event.rb b/db/migrate/057_add_map_bug_comment_event.rb index c88544099..087d0f692 100644 --- a/db/migrate/057_add_map_bug_comment_event.rb +++ b/db/migrate/057_add_map_bug_comment_event.rb @@ -1,6 +1,6 @@ class AddMapBugCommentEvent < ActiveRecord::Migration[4.2] def self.up - create_enumeration :map_bug_event_enum, %w[opened closed reopened commented hidden] + create_enum :map_bug_event_enum, %w[opened closed reopened commented hidden] add_column :map_bug_comment, :event, :map_bug_event_enum end diff --git a/db/migrate/20110521142405_rename_bugs_to_notes.rb b/db/migrate/20110521142405_rename_bugs_to_notes.rb index d3260dfa1..2e90b5ce8 100644 --- a/db/migrate/20110521142405_rename_bugs_to_notes.rb +++ b/db/migrate/20110521142405_rename_bugs_to_notes.rb @@ -1,7 +1,7 @@ class RenameBugsToNotes < ActiveRecord::Migration[4.2] def self.up - rename_enumeration "map_bug_status_enum", "note_status_enum" - rename_enumeration "map_bug_event_enum", "note_event_enum" + rename_enum "map_bug_status_enum", :to => "note_status_enum" + rename_enum "map_bug_event_enum", :to => "note_event_enum" rename_table :map_bugs, :notes rename_index :notes, "map_bugs_changed_idx", "notes_updated_at_idx" @@ -23,7 +23,7 @@ class RenameBugsToNotes < ActiveRecord::Migration[4.2] rename_index :notes, "notes_updated_at_idx", "map_bugs_changed_idx" rename_table :notes, :map_bugs - rename_enumeration "note_event_enum", "map_bug_event_enum" - rename_enumeration "note_status_enum", "map_bug_status_enum" + rename_enum "note_event_enum", :to => "map_bug_event_enum" + rename_enum "note_status_enum", :to => "map_bug_status_enum" end end diff --git a/db/migrate/20120214210114_add_text_format.rb b/db/migrate/20120214210114_add_text_format.rb index be8089a74..2fcb77d46 100644 --- a/db/migrate/20120214210114_add_text_format.rb +++ b/db/migrate/20120214210114_add_text_format.rb @@ -1,6 +1,6 @@ class AddTextFormat < ActiveRecord::Migration[4.2] def up - create_enumeration :format_enum, %w[html markdown text] + create_enum :format_enum, %w[html markdown text] add_column :users, :description_format, :format_enum, :null => false, :default => "html" add_column :user_blocks, :reason_format, :format_enum, :null => false, :default => "html" add_column :diary_entries, :body_format, :format_enum, :null => false, :default => "html" diff --git a/db/migrate/20160822153055_create_issues_and_reports.rb b/db/migrate/20160822153055_create_issues_and_reports.rb index 07a549820..83a9d9171 100644 --- a/db/migrate/20160822153055_create_issues_and_reports.rb +++ b/db/migrate/20160822153055_create_issues_and_reports.rb @@ -1,6 +1,6 @@ class CreateIssuesAndReports < ActiveRecord::Migration[5.0] def up - create_enumeration :issue_status_enum, %w[open ignored resolved] + create_enum :issue_status_enum, %w[open ignored resolved] create_table :issues do |t| t.string :reportable_type, :null => false diff --git a/db/migrate/20231029151516_add_importer_role.rb b/db/migrate/20231029151516_add_importer_role.rb new file mode 100644 index 000000000..9eaab1c05 --- /dev/null +++ b/db/migrate/20231029151516_add_importer_role.rb @@ -0,0 +1,5 @@ +class AddImporterRole < ActiveRecord::Migration[7.1] + def change + add_enum_value :user_role_enum, "importer" + end +end diff --git a/db/migrate/20231101222146_api_rate_limit.rb b/db/migrate/20231101222146_api_rate_limit.rb new file mode 100644 index 000000000..9790629ee --- /dev/null +++ b/db/migrate/20231101222146_api_rate_limit.rb @@ -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 diff --git a/db/structure.sql b/db/structure.sql index 17f269666..56e778523 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -91,7 +91,8 @@ CREATE TYPE public.nwr_enum AS ENUM ( CREATE TYPE public.user_role_enum AS ENUM ( 'administrator', - 'moderator' + 'moderator', + 'importer' ); @@ -107,6 +108,67 @@ CREATE TYPE public.user_status_enum AS ENUM ( 'deleted' ); + +-- +-- Name: api_rate_limit(bigint); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.api_rate_limit(user_id bigint) RETURNS integer + LANGUAGE plpgsql STABLE + AS $$ + DECLARE + min_changes_per_hour int4 := 100; + initial_changes_per_hour int4 := 1000; + max_changes_per_hour int4 := 100000; + days_to_max_changes int4 := 7; + importer_changes_per_hour int4 := 1000000; + moderator_changes_per_hour int4 := 1000000; + roles text[]; + last_block timestamp without time zone; + first_change timestamp without time zone; + active_reports int4; + time_since_first_change double precision; + max_changes double precision; + recent_changes int4; + BEGIN + SELECT ARRAY_AGG(user_roles.role) INTO STRICT roles FROM user_roles WHERE user_roles.user_id = api_rate_limit.user_id; + + IF 'moderator' = ANY(roles) THEN + max_changes := moderator_changes_per_hour; + ELSIF 'importer' = ANY(roles) THEN + max_changes := importer_changes_per_hour; + ELSE + SELECT user_blocks.created_at INTO last_block FROM user_blocks WHERE user_blocks.user_id = api_rate_limit.user_id ORDER BY user_blocks.created_at DESC LIMIT 1; + + IF FOUND THEN + SELECT changesets.created_at INTO first_change FROM changesets WHERE changesets.user_id = api_rate_limit.user_id AND changesets.created_at > last_block ORDER BY changesets.created_at LIMIT 1; + ELSE + SELECT changesets.created_at INTO first_change FROM changesets WHERE changesets.user_id = api_rate_limit.user_id ORDER BY changesets.created_at LIMIT 1; + END IF; + + IF NOT FOUND THEN + first_change := CURRENT_TIMESTAMP AT TIME ZONE 'UTC'; + END IF; + + SELECT COUNT(*) INTO STRICT active_reports + FROM issues INNER JOIN reports ON reports.issue_id = issues.id + WHERE issues.reported_user_id = api_rate_limit.user_id AND issues.status = 'open' AND reports.updated_at >= COALESCE(issues.resolved_at, '1970-01-01'); + + time_since_first_change := EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - first_change); + + max_changes := max_changes_per_hour * POWER(time_since_first_change, 2) / POWER(days_to_max_changes * 24 * 60 * 60, 2); + max_changes := GREATEST(initial_changes_per_hour, LEAST(max_changes_per_hour, FLOOR(max_changes))); + max_changes := max_changes / POWER(2, active_reports); + max_changes := GREATEST(min_changes_per_hour, LEAST(max_changes_per_hour, max_changes)); + END IF; + + SELECT COALESCE(SUM(changesets.num_changes), 0) INTO STRICT recent_changes FROM changesets WHERE changesets.user_id = api_rate_limit.user_id AND changesets.created_at >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - '1 hour'::interval; + + RETURN max_changes - recent_changes; + END; + $$; + + SET default_tablespace = ''; SET default_table_access_method = heap; @@ -3437,6 +3499,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('23'), ('22'), ('21'), +('20231101222146'), +('20231029151516'), ('20231010194809'), ('20231007141103'), ('20230830115220'), diff --git a/docker/postgres/Dockerfile b/docker/postgres/Dockerfile index 9e7479848..ceea80e9e 100644 --- a/docker/postgres/Dockerfile +++ b/docker/postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:11 +FROM postgres:14 # Add db init script to install OSM-specific Postgres user. ADD docker/postgres/openstreetmap-postgres-init.sh /docker-entrypoint-initdb.d/ diff --git a/lib/database_functions.rb b/lib/database_functions.rb new file mode 100644 index 000000000..f9e09ac70 --- /dev/null +++ b/lib/database_functions.rb @@ -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 diff --git a/lib/tasks/update_functions.rake b/lib/tasks/update_functions.rake new file mode 100644 index 000000000..605d3c9ad --- /dev/null +++ b/lib/tasks/update_functions.rake @@ -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 diff --git a/test/controllers/api/changesets_controller_test.rb b/test/controllers/api/changesets_controller_test.rb index 802e006e1..8efa37d87 100644 --- a/test/controllers/api/changesets_controller_test.rb +++ b/test/controllers/api/changesets_controller_test.rb @@ -1606,6 +1606,107 @@ module Api assert_equal "Precondition failed: Node #{node.id} is still used by ways #{way.id}.", @response.body end + ## + # test initial rate limit + def test_upload_initial_rate_limit + # create a user + user = create(:user) + + # create some objects to use + node = create(:node) + way = create(:way_with_nodes, :nodes_count => 2) + relation = create(:relation) + + # create a changeset that puts us near the initial rate limit + changeset = create(:changeset, :user => user, + :created_at => Time.now.utc - 5.minutes, + :num_changes => Settings.initial_changes_per_hour - 2) + + # create authentication header + auth_header = basic_authorization_header user.email, "test" + + # simple diff to create a node way and relation using placeholders + diff = <<~CHANGESET + + + + + + + + + + + + + + + + + + + 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 + + + + + + + + + + + + + + + + + + + CHANGESET + + # upload it + post changeset_upload_path(changeset), :params => diff, :headers => auth_header + assert_response :too_many_requests, "upload did not hit rate limit" + end + ## # when we make some simple changes we get the same changes back from the # diff download. @@ -2183,7 +2284,11 @@ module Api # check that a changeset can contain a certain max number of changes. ## FIXME should be changed to an integration test due to the with_controller def test_changeset_limits - auth_header = basic_authorization_header create(:user).email, "test" + user = create(:user) + auth_header = basic_authorization_header user.email, "test" + + # create an old changeset to ensure we have the maximum rate limit + create(:changeset, :user => user, :created_at => Time.now.utc - 28.days) # open a new changeset xml = "" diff --git a/test/controllers/api/nodes_controller_test.rb b/test/controllers/api/nodes_controller_test.rb index 2d827a077..7becffddc 100644 --- a/test/controllers/api/nodes_controller_test.rb +++ b/test/controllers/api/nodes_controller_test.rb @@ -558,6 +558,91 @@ module Api assert_includes apinode.tags, "\#{@user.inspect}" end + ## + # test initial rate limit + def test_initial_rate_limit + # create a user + user = create(:user) + + # create a changeset that puts us near the initial rate limit + changeset = create(:changeset, :user => user, + :created_at => Time.now.utc - 5.minutes, + :num_changes => Settings.initial_changes_per_hour - 1) + + # create authentication header + auth_header = basic_authorization_header user.email, "test" + + # try creating a node + xml = "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + 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 = "" + put node_create_path, :params => xml, :headers => auth_header + assert_response :too_many_requests, "node create did not hit rate limit" + end + private ## diff --git a/test/controllers/api/relations_controller_test.rb b/test/controllers/api/relations_controller_test.rb index cdef1f5ba..e6f507d3a 100644 --- a/test/controllers/api/relations_controller_test.rb +++ b/test/controllers/api/relations_controller_test.rb @@ -906,6 +906,117 @@ module Api end end + ## + # test initial rate limit + def test_initial_rate_limit + # create a user + user = create(:user) + + # create some nodes + node1 = create(:node) + node2 = create(:node) + + # create a changeset that puts us near the initial rate limit + changeset = create(:changeset, :user => user, + :created_at => Time.now.utc - 5.minutes, + :num_changes => Settings.initial_changes_per_hour - 1) + + # create authentication header + auth_header = basic_authorization_header user.email, "test" + + # try creating a relation + xml = "" \ + "" \ + "" \ + "" + 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 = "" \ + "" \ + "" \ + "" + 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 = "" + 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 = "" \ + "" \ + "" \ + "" + 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 = "" \ + "" \ + "" \ + "" + 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 = "" \ + "" \ + "" \ + "" + 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 = "" + 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 = "" \ + "" \ + "" \ + "" + 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) diff --git a/test/controllers/api/ways_controller_test.rb b/test/controllers/api/ways_controller_test.rb index 2bed0e5d6..791da8029 100644 --- a/test/controllers/api/ways_controller_test.rb +++ b/test/controllers/api/ways_controller_test.rb @@ -753,6 +753,111 @@ module Api end end + ## + # test initial rate limit + def test_initial_rate_limit + # create a user + user = create(:user) + + # create some nodes + node1 = create(:node) + node2 = create(:node) + + # create a changeset that puts us near the initial rate limit + changeset = create(:changeset, :user => user, + :created_at => Time.now.utc - 5.minutes, + :num_changes => Settings.initial_changes_per_hour - 1) + + # create authentication header + auth_header = basic_authorization_header user.email, "test" + + # try creating a way + xml = "" \ + "" \ + "" + 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 = "" \ + "" \ + "" + 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 = "" + 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 = "" \ + "" \ + "" + 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 = "" \ + "" \ + "" + 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 = "" \ + "" \ + "" + 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 = "" + 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 = "" \ + "" \ + "" + put way_create_path, :params => xml, :headers => auth_header + assert_response :too_many_requests, "way create did not hit rate limit" + end + private ## diff --git a/test/factories/user.rb b/test/factories/user.rb index cdc606cf1..166461637 100644 --- a/test/factories/user.rb +++ b/test/factories/user.rb @@ -47,6 +47,12 @@ FactoryBot.define do end end + factory :importer_user do + after(:create) do |user, _evaluator| + create(:user_role, :role => "importer", :user => user) + end + end + factory :moderator_user do after(:create) do |user, _evaluator| create(:user_role, :role => "moderator", :user => user) diff --git a/test/helpers/user_roles_helper_test.rb b/test/helpers/user_roles_helper_test.rb index 7708d5115..ba51dd14f 100644 --- a/test/helpers/user_roles_helper_test.rb +++ b/test/helpers/user_roles_helper_test.rb @@ -9,17 +9,27 @@ class UserRolesHelperTest < ActionView::TestCase icon = role_icon(current_user, "moderator") assert_dom_equal "", icon + icon = role_icon(current_user, "importer") + assert_dom_equal "", icon + icon = role_icon(create(:moderator_user), "moderator") expected = <<~HTML.delete("\n") This user is a moderator HTML assert_dom_equal expected, icon + + icon = role_icon(create(:importer_user), "importer") + expected = <<~HTML.delete("\n") + This user is a importer + 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") @@ -28,7 +38,16 @@ class UserRolesHelperTest < ActionView::TestCase HTML assert_dom_equal expected, icon + icon = role_icon(user, "importer") + expected = <<~HTML.delete("\n") + + Grant importer access + + HTML + assert_dom_equal expected, icon + moderator_user = create(:moderator_user) + icon = role_icon(moderator_user, "moderator") expected = <<~HTML.delete("\n") @@ -36,6 +55,32 @@ class UserRolesHelperTest < ActionView::TestCase HTML assert_dom_equal expected, icon + + icon = role_icon(user, "importer") + expected = <<~HTML.delete("\n") + + Grant importer access + + HTML + assert_dom_equal expected, icon + + importer_user = create(:importer_user) + + icon = role_icon(user, "moderator") + expected = <<~HTML.delete("\n") + + Grant moderator access + + HTML + assert_dom_equal expected, icon + + icon = role_icon(importer_user, "importer") + expected = <<~HTML.delete("\n") + + Revoke importer access + + HTML + assert_dom_equal expected, icon end def test_role_icons_normal @@ -50,10 +95,17 @@ class UserRolesHelperTest < ActionView::TestCase HTML assert_dom_equal expected, icons + icons = role_icons(create(:importer_user)) + expected = <<~HTML.delete("\n") + This user is a importer + HTML + assert_dom_equal expected, icons + icons = role_icons(create(:super_user)) expected = <<~HTML.delete("\n") This user is an administrator This user is a moderator + This user is a importer HTML assert_dom_equal expected, icons end @@ -70,6 +122,9 @@ class UserRolesHelperTest < ActionView::TestCase Grant moderator access + + Grant importer access + HTML assert_dom_equal expected, icons @@ -82,6 +137,24 @@ class UserRolesHelperTest < ActionView::TestCase Revoke moderator access + + Grant importer access + + HTML + assert_dom_equal expected, icons + + importer_user = create(:importer_user) + icons = role_icons(importer_user) + expected = <<~HTML.delete("\n") + + Grant administrator access + + + Grant moderator access + + + Revoke importer access + HTML assert_dom_equal expected, icons @@ -94,6 +167,9 @@ class UserRolesHelperTest < ActionView::TestCase Revoke moderator access + + Revoke importer access + HTML assert_dom_equal expected, icons end