users: require email verification for signups from proxies.

Require users who signup using proxies to verify their email addresses
before they can perform any edits. For verification purposes, the email
must be a nondisposable address from a whitelist of trusted email
providers.
This commit is contained in:
evazion
2020-03-24 02:18:37 -05:00
parent 5faa323729
commit b7bd6c8fdd
10 changed files with 83 additions and 2 deletions

View File

@@ -61,6 +61,7 @@ class UsersController < ApplicationController
def create def create
@user = authorize User.new( @user = authorize User.new(
last_ip_addr: CurrentUser.ip_addr, last_ip_addr: CurrentUser.ip_addr,
requires_verification: IpLookup.new(CurrentUser.ip_addr).is_proxy?,
name: params[:user][:name], name: params[:user][:name],
password: params[:user][:password], password: params[:user][:password],
password_confirmation: params[:user][:password_confirmation] password_confirmation: params[:user][:password_confirmation]

View File

@@ -76,6 +76,11 @@ module EmailValidator
"#{name}@#{domain}" "#{name}@#{domain}"
end end
def nondisposable?(address)
domain = Mail::Address.new(address).domain
domain.in?(Danbooru.config.email_domain_verification_list)
end
def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3) def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3)
mail_server = mx_domain(to_address, timeout: timeout) mail_server = mx_domain(to_address, timeout: timeout)
mail_server.nil? || rcpt_to_failed?(to_address, from_address, mail_server, timeout: timeout) mail_server.nil? || rcpt_to_failed?(to_address, from_address, mail_server, timeout: timeout)

View File

@@ -5,6 +5,10 @@ class IpLookup
attr_reader :ip_addr, :api_key, :cache_duration attr_reader :ip_addr, :api_key, :cache_duration
def self.enabled?
Danbooru.config.ip_registry_api_key.present?
end
def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 1.day) def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 1.day)
@ip_addr = ip_addr @ip_addr = ip_addr
@api_key = api_key @api_key = api_key

View File

@@ -2,24 +2,33 @@ class EmailAddress < ApplicationRecord
# https://www.regular-expressions.info/email.html # https://www.regular-expressions.info/email.html
EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/ EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
belongs_to :user belongs_to :user, inverse_of: :email_address
validates :address, presence: true, confirmation: true, format: { with: EMAIL_REGEX } validates :address, presence: true, confirmation: true, format: { with: EMAIL_REGEX }
validates :normalized_address, uniqueness: true validates :normalized_address, uniqueness: true
validates :user_id, uniqueness: true validates :user_id, uniqueness: true
validate :validate_deliverable, on: :deliverable validate :validate_deliverable, on: :deliverable
after_save :update_user
def address=(value) def address=(value)
self.normalized_address = EmailValidator.normalize(value) || address self.normalized_address = EmailValidator.normalize(value) || address
super super
end end
def nondisposable?
EmailValidator.nondisposable?(address)
end
def validate_deliverable def validate_deliverable
if EmailValidator.undeliverable?(address) if EmailValidator.undeliverable?(address)
errors[:address] << "is invalid or does not exist" errors[:address] << "is invalid or does not exist"
end end
end end
def update_user
user.update!(is_verified: is_verified? && nondisposable?)
end
concerning :VerificationMethods do concerning :VerificationMethods do
def verifier def verifier
@verifier ||= Danbooru::MessageVerifier.new(:email_verification_key) @verifier ||= Danbooru::MessageVerifier.new(:email_verification_key)

View File

@@ -63,6 +63,8 @@ class User < ApplicationRecord
opt_out_tracking opt_out_tracking
no_flagging no_flagging
no_feedback no_feedback
requires_verification
is_verified
) )
has_bit_flags BOOLEAN_ATTRIBUTES, :field => "bit_prefs" has_bit_flags BOOLEAN_ATTRIBUTES, :field => "bit_prefs"

View File

@@ -39,7 +39,11 @@ class ApplicationPolicy
end end
def unbanned? def unbanned?
user.is_member? && !user.is_banned? user.is_member? && !user.is_banned? && verified?
end
def verified?
user.is_verified? || user.is_gold? || !user.requires_verification?
end end
def policy(object) def policy(object)

View File

@@ -519,6 +519,13 @@ module Danbooru
nil nil
end end
# A list of email domains that are used for account verification purposes.
# If a user signs up from a proxy they will need to verify their account
# using an email address from one of the domains on this list.
def email_domain_verification_list
# ["gmail.com", "outlook.com", "yahoo.com"]
end
# API key for Google Maps. Used for embedding maps on IP address lookup pages. # API key for Google Maps. Used for embedding maps on IP address lookup pages.
# Generate at https://console.developers.google.com/apis/credentials # Generate at https://console.developers.google.com/apis/credentials
def google_maps_api_key def google_maps_api_key

View File

@@ -85,6 +85,30 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, @user.reload.email_address.is_verified) assert_equal(false, @user.reload.email_address.is_verified)
end end
end end
context "with a nondisposable email address" do
should "mark the user as verified" do
Danbooru.config.stubs(:email_domain_verification_list).returns(["gmail.com"])
@user.email_address.update!(address: "test@gmail.com")
get email_verification_url(@user)
assert_redirected_to @user
assert_equal(true, @user.reload.email_address.is_verified)
assert_equal(true, @user.is_verified)
end
end
context "with a disposable email address" do
should "not mark the user as verified" do
Danbooru.config.stubs(:email_domain_verification_list).returns([])
@user.email_address.update!(address: "test@mailinator.com")
get email_verification_url(@user)
assert_redirected_to @user
assert_equal(true, @user.reload.email_address.is_verified)
assert_equal(false, @user.is_verified)
end
end
end end
end end
end end

View File

@@ -311,6 +311,13 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
assert_response 403 assert_response 403
assert_not_equal("blah", @post.reload.tag_string) assert_not_equal("blah", @post.reload.tag_string)
end end
should "not allow unverified users to update posts" do
@user.update!(requires_verification: true, is_verified: false)
put_auth post_path(@post), @user, params: { post: { tag_string: "blah" }}
assert_response 403
assert_not_equal("blah", @post.reload.tag_string)
end
end end
context "revert action" do context "revert action" do

View File

@@ -151,6 +151,24 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end end
end end
should "mark users signing up from proxies as requiring verification" do
skip unless IpLookup.enabled?
self.remote_addr = "1.1.1.1"
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
assert_redirected_to User.last
assert_equal(true, User.last.requires_verification)
end
should "not mark users signing up from non-proxies as requiring verification" do
skip unless IpLookup.enabled?
self.remote_addr = "187.37.226.17"
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
assert_redirected_to User.last
assert_equal(false, User.last.requires_verification)
end
context "with sockpuppet validation enabled" do context "with sockpuppet validation enabled" do
setup do setup do
Danbooru.config.unstub(:enable_sock_puppet_validation?) Danbooru.config.unstub(:enable_sock_puppet_validation?)