From e0c17b7bd9e40791544eb0905c203e3adf84779b Mon Sep 17 00:00:00 2001 From: Anton Khorev Date: Fri, 20 Oct 2023 13:07:42 +0300 Subject: [PATCH] Add user block api call --- app/abilities/api_capability.rb | 3 +- app/controllers/api/user_blocks_controller.rb | 28 +++ config/locales/en.yml | 1 + config/routes.rb | 2 +- lib/oauth.rb | 4 +- .../api/user_blocks_controller_test.rb | 193 ++++++++++++++++++ 6 files changed, 227 insertions(+), 4 deletions(-) diff --git a/app/abilities/api_capability.rb b/app/abilities/api_capability.rb index f27dd2e63a5..8cab49f159e 100644 --- a/app/abilities/api_capability.rb +++ b/app/abilities/api_capability.rb @@ -29,7 +29,8 @@ def initialize(token) if user.moderator? can [:destroy, :restore], ChangesetComment if scope?(token, :write_api) can :destroy, Note if scope?(token, :write_notes) - can :redact, [OldNode, OldWay, OldRelation] if user&.terms_agreed? && (scope?(token, :write_api) || scope?(token, :write_redactions)) + can :redact, [OldNode, OldWay, OldRelation] if user.terms_agreed? && (scope?(token, :write_api) || scope?(token, :write_redactions)) + can :create, UserBlock if scope?(token, :write_blocks) end end end diff --git a/app/controllers/api/user_blocks_controller.rb b/app/controllers/api/user_blocks_controller.rb index 6c285e14a25..56edb861692 100644 --- a/app/controllers/api/user_blocks_controller.rb +++ b/app/controllers/api/user_blocks_controller.rb @@ -1,5 +1,8 @@ module Api class UserBlocksController < ApiController + before_action :check_api_writable, :only => [:create] + before_action :authorize, :only => [:create] + authorize_resource around_action :api_call_handle_error, :api_call_timeout @@ -12,5 +15,30 @@ def show rescue ActiveRecord::RecordNotFound raise OSM::APINotFoundError end + + def create + raise OSM::APIBadUserInput, "No user was given" unless params[:user] + + user = User.visible.find_by(:id => params[:user]) + raise OSM::APINotFoundError unless user + raise OSM::APIBadUserInput, "No reason was given" unless params[:reason] + raise OSM::APIBadUserInput, "No period was given" unless params[:period] + + period = Integer(params[:period], :exception => false) + raise OSM::APIBadUserInput, "Period should be a number of hours" unless period + + max_period = UserBlock::PERIODS.max + raise OSM::APIBadUserInput, "Period must be between 0 and #{max_period}" if period.negative? || period > max_period + raise OSM::APIBadUserInput, "Needs_view must be true if provided" unless params[:needs_view].nil? || params[:needs_view] == "true" + + @user_block = UserBlock.create( + :user => user, + :creator => current_user, + :reason => params[:reason], + :ends_at => Time.now.utc + period.hours, + :needs_view => params[:needs_view] == "true" + ) + render :show + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index e8dde952137..4ad266b7081 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2646,6 +2646,7 @@ en: write_gpx: Upload GPS traces write_notes: Modify notes write_redactions: Redact map data + write_blocks: Create and revoke user blocks read_email: Read user email address skip_authorization: Auto approve application for_roles: diff --git a/config/routes.rb b/config/routes.rb index a8d2b1d7e6c..8f3ebd96129 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -105,7 +105,7 @@ get "notes/getGPX" => "notes#index", :format => "gpx" get "notes/getRSSfeed" => "notes#feed", :format => "rss" - resources :user_blocks, :only => [:show], :constraints => { :id => /\d+/ }, :controller => "user_blocks", :as => :api_user_blocks + resources :user_blocks, :only => [:show, :create], :constraints => { :id => /\d+/ }, :controller => "user_blocks", :as => :api_user_blocks end # Data browsing diff --git a/lib/oauth.rb b/lib/oauth.rb index 88db38eb4bf..c217fed7232 100644 --- a/lib/oauth.rb +++ b/lib/oauth.rb @@ -1,8 +1,8 @@ module Oauth SCOPES = %w[read_prefs write_prefs write_diary write_api read_gpx write_gpx write_notes].freeze PRIVILEGED_SCOPES = %w[read_email skip_authorization].freeze - MODERATOR_SCOPES = %w[write_redactions].freeze - OAUTH2_SCOPES = %w[write_redactions openid].freeze + MODERATOR_SCOPES = %w[write_redactions write_blocks].freeze + OAUTH2_SCOPES = %w[write_redactions write_blocks openid].freeze class Scope attr_reader :name diff --git a/test/controllers/api/user_blocks_controller_test.rb b/test/controllers/api/user_blocks_controller_test.rb index 418ca03a1ee..656e77e25dd 100644 --- a/test/controllers/api/user_blocks_controller_test.rb +++ b/test/controllers/api/user_blocks_controller_test.rb @@ -3,6 +3,10 @@ module Api class UserBlocksControllerTest < ActionDispatch::IntegrationTest def test_routes + assert_routing( + { :path => "/api/0.6/user_blocks", :method => :post }, + { :controller => "api/user_blocks", :action => "create" } + ) assert_routing( { :path => "/api/0.6/user_blocks/1", :method => :get }, { :controller => "api/user_blocks", :action => "show", :id => "1" } @@ -43,5 +47,194 @@ def test_show_not_found assert_response :not_found assert_equal "text/plain", @response.media_type end + + def test_create_no_permission + blocked_user = create(:user) + assert_empty blocked_user.blocks + + post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1) + assert_response :unauthorized + assert_empty blocked_user.blocks + + regular_creator_user = create(:user) + token = create(:oauth_access_token, + :resource_owner_id => regular_creator_user.id, + :scopes => %w[read_prefs]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header + assert_response :forbidden + assert_empty blocked_user.blocks + + token = create(:oauth_access_token, + :resource_owner_id => regular_creator_user.id, + :scopes => %w[read_prefs write_blocks]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header + assert_response :forbidden + assert_empty blocked_user.blocks + + moderator_creator_user = create(:moderator_user) + token = create(:oauth_access_token, + :resource_owner_id => moderator_creator_user.id, + :scopes => %w[read_prefs]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header + assert_response :forbidden + assert_empty blocked_user.blocks + end + + def test_create_invalid_because_no_user + blocked_user = create(:user, :deleted) + assert_empty blocked_user.blocks + + creator_user = create(:moderator_user) + token = create(:oauth_access_token, + :resource_owner_id => creator_user.id, + :scopes => %w[read_prefs write_blocks]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:reason => "because", :period => 1), :headers => auth_header + assert_response :bad_request + assert_equal "text/plain", @response.media_type + assert_equal "No user was given", @response.body + + assert_empty blocked_user.blocks + end + + def test_create_invalid_because_user_is_unknown + creator_user = create(:moderator_user) + token = create(:oauth_access_token, + :resource_owner_id => creator_user.id, + :scopes => %w[read_prefs write_blocks]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:user => 0, :reason => "because", :period => 1), :headers => auth_header + assert_response :not_found + assert_equal "text/plain", @response.media_type + end + + def test_create_invalid_because_user_is_deleted + blocked_user = create(:user, :deleted) + assert_empty blocked_user.blocks + + creator_user = create(:moderator_user) + token = create(:oauth_access_token, + :resource_owner_id => creator_user.id, + :scopes => %w[read_prefs write_blocks]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header + assert_response :not_found + assert_equal "text/plain", @response.media_type + + assert_empty blocked_user.blocks + end + + def test_create_invalid_because_missing_reason + create_with_params_and_assert_bad_request("No reason was given", :period => "10") + end + + def test_create_invalid_because_missing_period + create_with_params_and_assert_bad_request("No period was given", :reason => "because") + end + + def test_create_invalid_because_non_numeric_period + create_with_params_and_assert_bad_request("Period should be a number of hours", :reason => "because", :period => "one hour") + end + + def test_create_invalid_because_negative_period + create_with_params_and_assert_bad_request("Period must be between 0 and #{UserBlock::PERIODS.max}", :reason => "go away", :period => "-1") + end + + def test_create_invalid_because_excessive_period + create_with_params_and_assert_bad_request("Period must be between 0 and #{UserBlock::PERIODS.max}", :reason => "go away", :period => "10000000") + end + + def test_create_invalid_because_unknown_needs_view + create_with_params_and_assert_bad_request("Needs_view must be true if provided", :reason => "because", :period => "1", :needs_view => "maybe") + end + + private + + def create_with_params_and_assert_bad_request(message, **params) + blocked_user = create(:user) + assert_empty blocked_user.blocks + + moderator_creator_user = create(:moderator_user) + token = create(:oauth_access_token, + :resource_owner_id => moderator_creator_user.id, + :scopes => %w[read_prefs write_blocks]) + auth_header = bearer_authorization_header(token.token) + + post api_user_blocks_path({ :user => blocked_user.id }.merge(params)), :headers => auth_header + assert_response :bad_request + assert_equal "text/plain", @response.media_type + assert_equal message, @response.body + + assert_empty blocked_user.blocks + end + + public + + def test_create_success + blocked_user = create(:user) + creator_user = create(:moderator_user) + + assert_empty blocked_user.blocks + token = create(:oauth_access_token, + :resource_owner_id => creator_user.id, + :scopes => %w[read_prefs write_blocks]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header + assert_response :success + assert_equal 1, blocked_user.blocks.length + + block = blocked_user.blocks.take + assert_predicate block, :active? + assert_equal "because", block.reason + assert_equal creator_user, block.creator + + assert_equal "application/xml", @response.media_type + assert_select "osm>user_block", 1 do + assert_select ">@id", block.id.to_s + assert_select ">@needs_view", "false" + assert_select ">user", 1 + assert_select ">user>@uid", blocked_user.id.to_s + assert_select ">creator", 1 + assert_select ">creator>@uid", creator_user.id.to_s + assert_select ">revoker", 0 + assert_select ">reason", 1 + assert_select ">reason", "because" + end + end + + def test_create_success_with_needs_view + blocked_user = create(:user) + creator_user = create(:moderator_user) + + assert_empty blocked_user.blocks + token = create(:oauth_access_token, + :resource_owner_id => creator_user.id, + :scopes => %w[read_prefs write_blocks]) + auth_header = bearer_authorization_header(token.token) + post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => "1", :needs_view => "true"), :headers => auth_header + assert_response :success + assert_equal 1, blocked_user.blocks.length + + block = blocked_user.blocks.take + assert_predicate block, :active? + assert_equal "because", block.reason + assert_equal creator_user, block.creator + + assert_equal "application/xml", @response.media_type + assert_select "osm>user_block", 1 do + assert_select ">@id", block.id.to_s + assert_select ">@needs_view", "true" + assert_select ">user", 1 + assert_select ">user>@uid", blocked_user.id.to_s + assert_select ">creator", 1 + assert_select ">creator>@uid", creator_user.id.to_s + assert_select ">revoker", 0 + assert_select ">reason", 1 + assert_select ">reason", "because" + end + end end end