Added the ability to block sign ups using a domain blacklist.

This commit is contained in:
Patricio Cano 2016-07-14 13:19:40 -05:00
parent 777a080892
commit defb8660c0
10 changed files with 232 additions and 37 deletions

View File

@ -64,6 +64,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:disabled_oauth_sign_in_sources] =
AuthHelper.button_based_providers.map(&:to_s) -
Array(enabled_oauth_sign_in_sources)
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
:default_projects_limit,
@ -112,6 +113,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:container_registry_token_expire_delay,
:repository_storage,
:enabled_git_access_protocol,
:domain_blacklist_enabled,
:domain_blacklist_raw,
:domain_blacklist_file,
restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: []

View File

@ -9,7 +9,9 @@ class ApplicationSetting < ActiveRecord::Base
serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array
serialize :restricted_signup_domains, Array
attr_accessor :restricted_signup_domains_raw
serialize :domain_blacklist, Array
attr_accessor :restricted_signup_domains_raw, :domain_blacklist_raw
validates :session_expire_delay,
presence: true,
@ -62,6 +64,10 @@ class ApplicationSetting < ActiveRecord::Base
validates :enabled_git_access_protocol,
inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true }
validates :domain_blacklist,
presence: true,
if: :domain_blacklist_enabled?
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
@ -154,18 +160,35 @@ class ApplicationSetting < ActiveRecord::Base
self.restricted_signup_domains.join("\n") unless self.restricted_signup_domains.nil?
end
def restricted_signup_domains_raw=(values)
self.restricted_signup_domains = []
self.restricted_signup_domains = values.split(
/\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
def domain_blacklist_raw
self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
end
def splitter
/\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
/x)
/x
end
def restricted_signup_domains_raw=(values)
self.restricted_signup_domains = []
self.restricted_signup_domains = values.split(splitter)
self.restricted_signup_domains.reject! { |d| d.empty? }
end
def domain_blacklist_raw=(values)
self.domain_blacklist = []
self.domain_blacklist = values.split(splitter)
self.domain_blacklist.reject! { |d| d.empty? }
end
def domain_blacklist_file=(file)
self.domain_blacklist_raw = file.read
end
def runners_registration_token
ensure_runners_registration_token!
end

View File

@ -111,7 +111,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create
before_validation :signup_domain_valid?, on: :create
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
@ -760,27 +760,41 @@ class User < ActiveRecord::Base
Project.where(id: events)
end
def restricted_signup_domains
email_domains = current_application_settings.restricted_signup_domains
def match_domain(email_domains)
email_domains.any? do |domain|
escaped = Regexp.escape(domain).gsub('\*', '.*?')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
email_domain = Mail::Address.new(self.email).domain
email_domain =~ regexp
end
end
unless email_domains.blank?
match_found = email_domains.any? do |domain|
escaped = Regexp.escape(domain).gsub('\*', '.*?')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
email_domain = Mail::Address.new(self.email).domain
email_domain =~ regexp
end
def signup_domain_valid?
valid = true
unless match_found
self.errors.add :email,
'is not whitelisted. ' +
'Email domains valid for registration are: ' +
email_domains.join(', ')
return false
if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist
if match_domain(blocked_domains)
self.errors.add :email, 'is not from an allowed domain.'
valid = false
end
end
true
allowed_domains = current_application_settings.restricted_signup_domains
unless allowed_domains.blank?
if match_domain(allowed_domains)
self.errors.clear
valid = true
else
self.errors.add :email,
'is not whitelisted. ' +
'Email domains valid for registration are: ' +
allowed_domains.join(', ')
valid = false
end
end
return valid
end
def can_be_removed?

View File

@ -109,7 +109,7 @@
Newly registered users will by default be external
%fieldset
%legend Sign-in Restrictions
%legend Sign-up Restrictions
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@ -122,6 +122,49 @@
= f.label :send_user_confirmation_email do
= f.check_box :send_user_confirmation_email
Send confirmation email on sign-up
.form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
.help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
= f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :domain_blacklist_enabled do
= f.check_box :domain_blacklist_enabled
Enable domain blacklist for sign ups
.form-group
.col-sm-offset-2.col-sm-10
.radio
= label_tag :blacklist_type_file do
= radio_button_tag :blacklist_type, :file, @application_setting.domain_blacklist.blank?
.option-title
Upload blacklist file
.radio
= label_tag :blacklist_type_raw do
= radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present?
.option-title
Enter blacklist manually
.form-group.blacklist-file
= f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
.col-sm-10
= f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
.help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
.form-group.blacklist-raw
= f.label :domain_blacklist, 'Blacklisted domains', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 10
.help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
= f.label :after_sign_up_text, class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
.help-block Markdown enabled
%fieldset
%legend Sign-in Restrictions
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@ -147,11 +190,6 @@
.col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
.help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
.form-group
= f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
.col-sm-10
@ -167,11 +205,6 @@
.col-sm-10
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.help-block Markdown enabled
.form-group
= f.label :after_sign_up_text, class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
.help-block Markdown enabled
.form-group
= f.label :help_page_text, class: 'control-label col-sm-2'
.col-sm-10
@ -353,3 +386,21 @@
.form-actions
= f.submit 'Save', class: 'btn btn-save'
:javascript
function showBlacklistType() {
if ($("input[name='blacklist_type']:checked").val() == "file")
{
$(".blacklist-file").show();
$(".blacklist-raw").hide();
}
else
{
$(".blacklist-file").hide();
$(".blacklist-raw").show();
}
}
$("input[name='blacklist_type']").click(showBlacklistType);
showBlacklistType();

View File

@ -0,0 +1,22 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :application_settings, :domain_blacklist_enabled, :boolean, default: false
add_column :application_settings, :domain_blacklist, :text
end
end

View File

@ -88,6 +88,8 @@ ActiveRecord::Schema.define(version: 20160716115710) do
t.text "after_sign_up_text"
t.string "repository_storage", default: "default"
t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
end
create_table "audit_events", force: :cascade do |t|

View File

@ -413,6 +413,8 @@ module API
expose :default_snippet_visibility
expose :default_group_visibility
expose :restricted_signup_domains
expose :domain_blacklist_enabled
expose :domain_blacklist
expose :user_oauth_applications
expose :after_sign_out_path
expose :container_registry_token_expire_delay

3
spec/fixtures/blacklist.txt vendored Normal file
View File

@ -0,0 +1,3 @@
example.com
test.com
foo.bar

View File

@ -73,4 +73,31 @@ describe ApplicationSetting, models: true do
expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
end
end
context 'blacklisted signup domains' do
it 'set single domain' do
setting.domain_blacklist_raw = 'example.com'
expect(setting.domain_blacklist).to eq(['example.com'])
end
it 'set multiple domains with spaces' do
setting.domain_blacklist_raw = 'example.com *.example.com'
expect(setting.domain_blacklist).to eq(['example.com', '*.example.com'])
end
it 'set multiple domains with newlines and a space' do
setting.domain_blacklist_raw = "example.com\n *.example.com"
expect(setting.domain_blacklist).to eq(['example.com', '*.example.com'])
end
it 'set multiple domains with commas' do
setting.domain_blacklist_raw = "example.com, *.example.com"
expect(setting.domain_blacklist).to eq(['example.com', '*.example.com'])
end
it 'set multiple domain with file' do
setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'blacklist.txt'))
expect(setting.domain_blacklist).to eq(%w(example.com test.com foo.bar))
end
end
end

View File

@ -89,7 +89,7 @@ describe User, models: true do
end
describe 'email' do
context 'when no signup domains listed' do
context 'when no signup domains white listed' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([])
end
@ -100,7 +100,7 @@ describe User, models: true do
end
end
context 'when a signup domain is listed and subdomains are allowed' do
context 'when a signup domain is white listed and subdomains are allowed' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com'])
end
@ -121,7 +121,7 @@ describe User, models: true do
end
end
context 'when a signup domain is listed and subdomains are not allowed' do
context 'when a signup domain is white listed and subdomains are not allowed' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com'])
end
@ -142,6 +142,53 @@ describe User, models: true do
end
end
context 'domain blacklist' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist_enabled?).and_return(true)
allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['example.com'])
end
context 'when a signup domain is black listed' do
it 'accepts info@test.com' do
user = build(:user, email: 'info@test.com')
expect(user).to be_valid
end
it 'rejects info@example.com' do
user = build(:user, email: 'info@example.com')
expect(user).not_to be_valid
end
end
context 'when a signup domain is black listed but a wildcard subdomain is allowed' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['test.example.com'])
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['*.example.com'])
end
it 'should give priority to whitelist and allow info@test.example.com' do
user = build(:user, email: 'info@test.example.com')
expect(user).to be_valid
end
end
context 'with both lists containing a domain' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['test.com'])
end
it 'accepts info@test.com' do
user = build(:user, email: 'info@test.com')
expect(user).to be_valid
end
it 'rejects info@example.com' do
user = build(:user, email: 'info@example.com')
expect(user).not_to be_valid
end
end
end
context 'owns_notification_email' do
it 'accepts temp_oauth_email emails' do
user = build(:user, email: "temp-email-for-oauth@example.com")