diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index 4962f8a8c70..21be74069c9 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -12,6 +12,7 @@ def initialize(user) can [:index, :permalink, :edit, :help, :fixthemap, :offline, :export, :about, :communities, :preview, :copyright, :id], :site can [:create, :show], :export can [:create, :read], :search + can [:create, :show], :auth_deletion if Settings.status != "database_offline" can [:read, :feed], Changeset diff --git a/app/controllers/accounts/auth_deletions_controller.rb b/app/controllers/accounts/auth_deletions_controller.rb new file mode 100644 index 00000000000..6cffe789ccd --- /dev/null +++ b/app/controllers/accounts/auth_deletions_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Accounts + class AuthDeletionsController < ApplicationController + layout :site_layout + + before_action :set_locale + + authorize_resource :class => :auth_deletion + + def show + @auth_provider = params.expect(:provider) + @auth_uid, @time = Rails + .application + .message_verifier(:social_login_deletion) + .verify(params.expect(:confirmation_code)) + rescue ActiveSupport::MessageVerifier::InvalidSignature + head :bad_request + end + + def create + if params.expect(:provider) == "facebook" + create_facebook + else + head :not_found + end + end + + private + + def create_facebook + encoded_signature, payload = params.expect(:signed_request).split(".", 2) + signature = Base64.urlsafe_decode64(encoded_signature) + + raise ActionController::BadRequest unless signature == OpenSSL::HMAC.digest("SHA256", Settings.facebook_auth_secret, payload) + + data = JSON.parse(Base64.urlsafe_decode64(payload)) + user = User.find_by!(:auth_provider => "facebook", :auth_uid => data["user_id"]) + + user.auth_provider = nil + user.auth_uid = nil + user.save! + + @confirmation_code = Rails + .application + .message_verifier(:social_login_deletion) + .generate([data["user_id"], Time.now.to_i]) + + render :formats => [:json] + end + end +end diff --git a/app/views/accounts/auth_deletions/create.json.jbuilder b/app/views/accounts/auth_deletions/create.json.jbuilder new file mode 100644 index 00000000000..f7b64f357e3 --- /dev/null +++ b/app/views/accounts/auth_deletions/create.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +json.url auth_delete_url(:provider => params[:provider], + :confirmation_code => @confirmation_code) +json.confirmation_code @confirmation_code diff --git a/app/views/accounts/auth_deletions/show.html.erb b/app/views/accounts/auth_deletions/show.html.erb new file mode 100644 index 00000000000..d5ef280d0c0 --- /dev/null +++ b/app/views/accounts/auth_deletions/show.html.erb @@ -0,0 +1,7 @@ +<% content_for :heading do %> +

<%= t ".title" %>

+<% end %> + +

+ <%= t ".body", :provider => t("auth.providers.#{@auth_provider}"), :id => @auth_uid, :time => l(Time.at(@time).utc) -%> +

diff --git a/config/locales/en.yml b/config/locales/en.yml index b0100044464..8d5d4524b77 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -336,6 +336,10 @@ en: successfully_declared: "You have successfully declared that you consider your edits to be in the Public Domain." already_declared: "You have already declared that you consider your edits to be in the Public Domain." did_not_confirm: "You didn't confirm that you consider your edits to be in the Public Domain." + auth_deletions: + show: + title: "Data deletion request" + body: "Data for %{provider} ID %{id} was removed at %{time} and it was disconnected from the associated OpenStreetMap account." browse: deleted_ago_by_html: "Deleted %{time_ago} by %{user}" edited_ago_by_html: "Edited %{time_ago} by %{user}" diff --git a/config/routes.rb b/config/routes.rb index e63bed58e68..7f4f7160e67 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -233,9 +233,16 @@ get "/forgot-password.html", :to => redirect(:path => "/user/forgot-password") # omniauth - get "/auth/failure" => "users#auth_failure" - match "/auth/:provider/callback" => "users#auth_success", :via => [:get, :post], :as => :auth_success - match "/auth/:provider" => "users#auth", :via => [:post, :patch], :as => :auth + scope "/auth", :as => :auth do + get "/failure" => "users#auth_failure" + + scope ":provider" do + match "/callback" => "users#auth_success", :via => [:get, :post], :as => :success + match "" => "users#auth", :via => [:post, :patch] + + resource :delete, :only => [:show, :create], :module => "accounts", :controller => "auth_deletions" + end + end # permalink get "/go/:code" => "site#permalink", :code => /[a-zA-Z0-9_@~]+[=-]*/, :as => :permalink diff --git a/test/controllers/accounts/auth_deletions_controller_test.rb b/test/controllers/accounts/auth_deletions_controller_test.rb new file mode 100644 index 00000000000..437f1ff29fa --- /dev/null +++ b/test/controllers/accounts/auth_deletions_controller_test.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require "test_helper" + +module Accounts + class AuthDeletionsControllerTest < ActionDispatch::IntegrationTest + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/auth/provider/delete", :method => :get }, + { :controller => "accounts/auth_deletions", :action => "show", :provider => "provider" } + ) + assert_routing( + { :path => "/auth/provider/delete", :method => :post }, + { :controller => "accounts/auth_deletions", :action => "create", :provider => "provider" } + ) + end + + ## + # test showing status of a facebook deletion request + def test_show_facebook + user = create(:user, :auth_provider => "facebook", :auth_uid => "12345") + confirmation_code = Rails.application.message_verifier(:social_login_deletion).generate([user.auth_uid, Time.now.to_i]) + + get auth_delete_path(:provider => "facebook", :confirmation_code => confirmation_code) + assert_response :success + assert_select "p", /^Data for Facebook ID 12345 was removed at .* and it was disconnected from the associated OpenStreetMap account\.$/ + end + + ## + # test showing status of a facebook deletion request with no code + def test_show_facebook_no_code + get auth_delete_path(:provider => "facebook") + assert_response :bad_request + end + + ## + # test showing status of a facebook deletion request with an invalid code + def test_show_facebook_invalid_code + get auth_delete_path(:provider => "facebook", :confirmation_code => "invalid") + assert_response :bad_request + end + + ## + # test creation of a facebook deletion request + def test_create_facebook + user = create(:user, :auth_provider => "facebook", :auth_uid => "12345") + + payload = Base64.urlsafe_encode64( + JSON.generate( + :algorithm => "HMAC-SHA256", + :expires => Time.now.to_i + 3600, + :issued_at => Time.now.to_i, + :user_id => "12345" + ) + ) + signature = OpenSSL::HMAC.digest("SHA256", Settings.facebook_auth_secret, payload) + encoded_signature = Base64.urlsafe_encode64(signature) + signed_request = [encoded_signature, payload].join(".") + + post auth_delete_path(:provider => "facebook"), :params => { :signed_request => signed_request } + assert_response :success + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_not_nil js["confirmation_code"] + assert_equal auth_delete_url(:provider => "facebook", :confirmation_code => js["confirmation_code"]), js["url"] + + assert_nil user.reload.auth_provider + assert_nil user.reload.auth_uid + end + + ## + # test creation of a facebook deletion request with an invalid signature + def test_create_facebook_bad_signature + create(:user, :auth_provider => "facebook", :auth_uid => "12345") + + payload = Base64.urlsafe_encode64( + JSON.generate( + :algorithm => "HMAC-SHA256", + :expires => Time.now.to_i + 3600, + :issued_at => Time.now.to_i, + :user_id => "12345" + ) + ) + signature = OpenSSL::HMAC.digest("SHA256", "invalid secret", payload) + encoded_signature = Base64.urlsafe_encode64(signature) + signed_request = [encoded_signature, payload].join(".") + + post auth_delete_path(:provider => "facebook"), :params => { :signed_request => signed_request } + assert_response :bad_request + end + + ## + # test creation of a facebook deletion request for an unassociated ID + def test_create_facebook_not_associated + payload = Base64.urlsafe_encode64( + JSON.generate( + :algorithm => "HMAC-SHA256", + :expires => Time.now.to_i + 3600, + :issued_at => Time.now.to_i, + :user_id => "12345" + ) + ) + signature = OpenSSL::HMAC.digest("SHA256", Settings.facebook_auth_secret, payload) + encoded_signature = Base64.urlsafe_encode64(signature) + signed_request = [encoded_signature, payload].join(".") + + post auth_delete_path(:provider => "facebook"), :params => { :signed_request => signed_request } + assert_response :not_found + end + + ## + # test creation of a deletion request for an unsupported provider + def test_create_unsupported + post auth_delete_path(:provider => "unsupported") + assert_response :not_found + end + end +end