From 93b9bfd93a841b7f86e6aeab3f9c5e9ede3a4503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20R=C3=BCttimann?= Date: Thu, 30 Aug 2018 12:53:06 +0000 Subject: [PATCH] Allow whitelisting for "external collaborator by default" setting --- Gemfile | 3 + Gemfile.lock | 4 + Gemfile.rails5.lock | 4 + .../account_and_limits.js | 25 +++++ app/assets/javascripts/pages/admin/index.js | 6 +- .../pages/admin/users/new/index.js | 49 ++++++++ app/helpers/application_settings_helper.rb | 1 + app/helpers/users_helper.rb | 11 ++ app/models/application_setting.rb | 11 ++ app/services/users/build_service.rb | 12 ++ app/validators/js_regex_validator.rb | 15 +++ .../_account_and_limit.html.haml | 7 ++ .../admin/users/_access_levels.html.haml | 4 + ...eature-whitelist-new-users-as-internal.yml | 5 + ...r_internal_regex_to_application_setting.rb | 13 +++ db/schema.rb | 1 + doc/user/permissions.md | 17 ++- locale/gitlab.pot | 15 +++ spec/features/admin/admin_settings_spec.rb | 12 ++ spec/features/admin/admin_users_spec.rb | 46 ++++++++ spec/helpers/users_helper_spec.rb | 24 ++++ spec/javascripts/fixtures/admin_users.rb | 29 +++++ .../fixtures/application_settings.rb | 34 ++++++ .../account_and_limits_spec.js | 33 ++++++ .../pages/admin/users/new/index_spec.js | 43 +++++++ spec/models/application_setting_spec.rb | 24 ++++ spec/services/users/build_service_spec.rb | 106 ++++++++++++++++++ spec/validators/js_regex_validator_spec.rb | 27 +++++ 28 files changed, 579 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/pages/admin/application_settings/account_and_limits.js create mode 100644 app/assets/javascripts/pages/admin/users/new/index.js create mode 100644 app/validators/js_regex_validator.rb create mode 100644 changelogs/unreleased/feature-whitelist-new-users-as-internal.yml create mode 100644 db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb create mode 100644 spec/javascripts/fixtures/admin_users.rb create mode 100644 spec/javascripts/fixtures/application_settings.rb create mode 100644 spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js create mode 100644 spec/javascripts/pages/admin/users/new/index_spec.js create mode 100644 spec/validators/js_regex_validator_spec.rb diff --git a/Gemfile b/Gemfile index 208289cb7fb..8fbdb1917c7 100644 --- a/Gemfile +++ b/Gemfile @@ -195,6 +195,9 @@ gem 're2', '~> 1.1.1' gem 'version_sorter', '~> 2.1.0' +# Export Ruby Regex to Javascript +gem 'js_regex', '~> 2.2.1' + # User agent parsing gem 'device_detector' diff --git a/Gemfile.lock b/Gemfile.lock index 77effb63d2e..754b2e56de1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -428,6 +428,8 @@ GEM multipart-post oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) + js_regex (2.2.1) + regexp_parser (>= 0.4.11, <= 0.5.0) json (1.8.6) json-jwt (1.9.4) activesupport @@ -726,6 +728,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) + regexp_parser (0.5.0) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -1074,6 +1077,7 @@ DEPENDENCIES influxdb (~> 0.2) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) + js_regex (~> 2.2.1) json-schema (~> 2.8.0) jwt (~> 1.5.6) kaminari (~> 1.0) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 63b450d3f62..5091c671b8b 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -431,6 +431,8 @@ GEM multipart-post oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) + js_regex (2.2.1) + regexp_parser (>= 0.4.11, <= 0.5.0) json (1.8.6) json-jwt (1.9.4) activesupport @@ -735,6 +737,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) + regexp_parser (0.5.0) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -1084,6 +1087,7 @@ DEPENDENCIES influxdb (~> 0.2) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) + js_regex (~> 2.2.1) json-schema (~> 2.8.0) jwt (~> 1.5.6) kaminari (~> 1.0) diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js new file mode 100644 index 00000000000..7281f907ec7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js @@ -0,0 +1,25 @@ +import { __ } from '~/locale'; + +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern'); +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external'); + +function setUserInternalRegexPlaceholder(checkbox) { + const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex'); + if (checkbox && userInternalRegex) { + if (checkbox.checked) { + userInternalRegex.readOnly = false; + userInternalRegex.placeholder = PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE; + } else { + userInternalRegex.readOnly = true; + userInternalRegex.placeholder = PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE; + } + } +} + +export default function initUserInternalRegexPlaceholder() { + const checkbox = document.getElementById('application_setting_user_default_external'); + setUserInternalRegexPlaceholder(checkbox); + checkbox.addEventListener('change', () => { + setUserInternalRegexPlaceholder(checkbox); + }); +} diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js index e50b61f09e2..3aa793e47b9 100644 --- a/app/assets/javascripts/pages/admin/index.js +++ b/app/assets/javascripts/pages/admin/index.js @@ -1,3 +1,7 @@ import initAdmin from './admin'; +import initUserInternalRegexPlaceholder from './application_settings/account_and_limits'; -document.addEventListener('DOMContentLoaded', initAdmin); +document.addEventListener('DOMContentLoaded', () => { + initAdmin(); + initUserInternalRegexPlaceholder(); +}); diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js new file mode 100644 index 00000000000..58bfa8d64e7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/new/index.js @@ -0,0 +1,49 @@ +import $ from 'jquery'; + +export default class UserInternalRegexHandler { + constructor() { + this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern'); + if (this.regexPattern && this.regexPattern !== '') { + this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options'); + this.external = $('#user_external'); + this.warningMessage = $('#warning_external_automatically_set'); + this.addListenerToEmailField(); + this.addListenerToUserExternalCheckbox(); + } + } + + addListenerToEmailField() { + $('#user_email').on('input', (event) => { + this.setExternalCheckbox(event.currentTarget.value); + }); + } + + addListenerToUserExternalCheckbox() { + this.external.on('click', () => { + this.warningMessage.addClass('hidden'); + }); + } + + isEmailInternal(email) { + const regex = new RegExp(this.regexPattern, this.regexOptions); + return regex.test(email); + } + + setExternalCheckbox(email) { + const isChecked = this.external.prop('checked'); + if (this.isEmailInternal(email)) { + if (isChecked) { + this.external.prop('checked', false); + this.warningMessage.removeClass('hidden'); + } + } else if (!isChecked) { + this.external.prop('checked', true); + this.warningMessage.addClass('hidden'); + } + } +} + +document.addEventListener('DOMContentLoaded', () => { + // eslint-disable-next-line + new UserInternalRegexHandler(); +}); diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 1e05f07e676..684c84c3006 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -255,6 +255,7 @@ module ApplicationSettingsHelper :instance_statistics_visibility_private, :user_default_external, :user_show_add_ssh_key_message, + :user_default_internal_regex, :user_oauth_applications, :version_check_enabled, :web_ide_clientside_preview_enabled diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index ceea4384f91..2c0c4254a0c 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -23,6 +23,17 @@ module UsersHelper profile_tabs.include?(tab) end + def user_internal_regex_data + settings = Gitlab::CurrentSettings.current_application_settings + + pattern, options = if settings.user_default_internal_regex_enabled? + regex = settings.user_default_internal_regex_instance + JsRegex.new(regex).to_h.slice(:source, :options).values + end + + { user_internal_regex_pattern: pattern, user_internal_regex_options: options } + end + def current_user_menu_items @current_user_menu_items ||= get_current_user_menu_items end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c77faa4b71d..03bd7fa016e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -192,6 +192,8 @@ class ApplicationSetting < ActiveRecord::Base numericality: { less_than_or_equal_to: :gitaly_timeout_default }, if: :gitaly_timeout_default + validates :user_default_internal_regex, js_regex: true, allow_nil: true + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -299,6 +301,7 @@ class ApplicationSetting < ActiveRecord::Base usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], instance_statistics_visibility_private: false, user_default_external: false, + user_default_internal_regex: nil, user_show_add_ssh_key_message: true } end @@ -435,6 +438,14 @@ class ApplicationSetting < ActiveRecord::Base password_authentication_enabled_for_web? || password_authentication_enabled_for_git? end + def user_default_internal_regex_enabled? + user_default_external? && user_default_internal_regex.present? + end + + def user_default_internal_regex_instance + Regexp.new(user_default_internal_regex, Regexp::IGNORECASE) + end + delegate :terms, to: :latest_terms, allow_nil: true def latest_terms @latest_terms ||= Term.latest diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index acc2fa153ae..9417c63c43a 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -2,6 +2,10 @@ module Users class BuildService < BaseService + delegate :user_default_internal_regex_enabled?, + :user_default_internal_regex_instance, + to: :'Gitlab::CurrentSettings.current_application_settings' + def initialize(current_user, params = {}) @current_user = current_user @params = params.dup @@ -89,6 +93,10 @@ module Users if params[:reset_password] user_params.merge!(force_random_password: true, password_expires_at: nil) end + + if user_default_internal_regex_enabled? && !user_params.key?(:external) + user_params[:external] = user_external? + end else allowed_signup_params = signup_params allowed_signup_params << :skip_confirmation if skip_authorization @@ -105,5 +113,9 @@ module Users def skip_user_confirmation_email_from_setting !Gitlab::CurrentSettings.send_user_confirmation_email end + + def user_external? + user_default_internal_regex_instance.match(params[:email]).nil? + end end end diff --git a/app/validators/js_regex_validator.rb b/app/validators/js_regex_validator.rb new file mode 100644 index 00000000000..a515af7b919 --- /dev/null +++ b/app/validators/js_regex_validator.rb @@ -0,0 +1,15 @@ +class JsRegexValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return true if value.blank? + + parsed_regex = JsRegex.new(Regexp.new(value, Regexp::IGNORECASE)) + + if parsed_regex.source.empty? + record.errors.add(attribute, "Regex Pattern #{value} can not be expressed in Javascript") + else + parsed_regex.warnings.each { |warning| record.errors.add(attribute, warning) } + end + rescue RegexpError => regex_error + record.errors.add(attribute, regex_error.to_s) + end +end diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 622cb11010e..9121e44d31b 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -29,6 +29,13 @@ = f.check_box :user_default_external, class: 'form-check-input' = f.label :user_default_external, class: 'form-check-label' do Newly registered users will by default be external + .prepend-top-10 + = _('Internal users') + = f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control prepend-top-5' + .help-block + = _('Specify an e-mail address regex pattern to identify default internal users.') + = link_to _('More information'), help_page_path('user/permissions', anchor: 'external-users-permissions'), + target: '_blank' .form-group = f.label :user_show_add_ssh_key_message, 'Prompt users to upload SSH keys', class: 'label-bold' .form-check diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 5f68163163e..12e24ddef02 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -34,8 +34,12 @@ .form-group.row .col-sm-2.text-right = f.label :external, class: 'col-form-label' + .hidden{ data: user_internal_regex_data } .col-sm-10 = f.check_box :external do External %p.light External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. + %row.hidden#warning_external_automatically_set.hidden + .badge.badge-warning.text-white + = _('Automatically marked as default internal user') diff --git a/changelogs/unreleased/feature-whitelist-new-users-as-internal.yml b/changelogs/unreleased/feature-whitelist-new-users-as-internal.yml new file mode 100644 index 00000000000..7a3bd11c119 --- /dev/null +++ b/changelogs/unreleased/feature-whitelist-new-users-as-internal.yml @@ -0,0 +1,5 @@ +--- +title: Add an option to whitelist users based on email address as internal when the "New user set to external" setting is enabled. +merge_request: 17711 +author: Roger Rüttimann +type: added diff --git a/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb new file mode 100644 index 00000000000..fe50e909563 --- /dev/null +++ b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb @@ -0,0 +1,13 @@ +class AddUserInternalRegexToApplicationSetting < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :application_settings, :user_default_internal_regex, :string, null: true + end + + def down + remove_column :application_settings, :user_default_internal_regex + end +end diff --git a/db/schema.rb b/db/schema.rb index cb8f90efded..02e545bec7d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -164,6 +164,7 @@ ActiveRecord::Schema.define(version: 20180826111825) do t.boolean "authorized_keys_enabled", default: true, null: false t.string "auto_devops_domain" t.boolean "pages_domain_verification_enabled", default: true, null: false + t.string "user_default_internal_regex" t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false t.boolean "enforce_terms", default: false t.boolean "mirror_available", default: true, null: false diff --git a/doc/user/permissions.md b/doc/user/permissions.md index b6438397db8..10ac6301aa1 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -197,7 +197,7 @@ They will, like usual users, receive a role in the project or group with all the abilities that are mentioned in the table above. They cannot however create groups or projects, and they have the same access as logged out users in all other cases. - + An administrator can flag a user as external [through the API](../api/users.md) or by checking the checkbox on the admin panel. As an administrator, navigate to **Admin > Users** to create a new user or edit an existing one. There, you @@ -206,6 +206,21 @@ will find the option to flag the user as external. By default new users are not set as external users. This behavior can be changed by an administrator under **Admin > Application Settings**. +### Default internal users + +The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users. + +New users whose email address matches the regex pattern will be set to internal by default rather than an external collaborator. + +The regex pattern format is Ruby, but it needs to be convertible to JavaScript, and the ignore case flag will be set, e.g. "/regex pattern/i". + +Here are some examples: + +- Use `\.internal@domain\.com` to mark email addresses containing ".internal@domain.com" internal. +- Use `^(?:(?!\.ext@domain\.com).)*$\r?` to mark users with email addresses NOT including .ext@domain.com internal. + +Please be aware that this regex could lead to a DOS attack, [see](https://en.wikipedia.org/wiki/ReDoS?) ReDos on Wikipedia. + ## Auditor users **[PREMIUM ONLY]** >[Introduced][ee-998] in [GitLab Premium][eep] 8.17. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ce5d82d479b..b5778289b50 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -727,6 +727,9 @@ msgstr "" msgid "AutoDevOps|enable Auto DevOps" msgstr "" +msgid "Automatically marked as default internal user" +msgstr "" + msgid "Available" msgstr "" @@ -3187,6 +3190,9 @@ msgstr "" msgid "Internal - The project can be accessed by any logged in user." msgstr "" +msgid "Internal users" +msgstr "" + msgid "Interval Pattern" msgstr "" @@ -4670,6 +4676,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..." msgstr[0] "" msgstr[1] "" +msgid "Regex pattern" +msgstr "" + msgid "Register / Sign In" msgstr "" @@ -5277,6 +5286,9 @@ msgstr "" msgid "Specific Runners" msgstr "" +msgid "Specify an e-mail address regex pattern to identify default internal users." +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -5934,6 +5946,9 @@ msgstr "" msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}." msgstr "" +msgid "To define internal users, first enable new users set to external" +msgstr "" + msgid "To get started you enter your FogBugz URL and login information below. In the next steps, you'll be able to map users and select the projects you want to import." msgstr "" diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index af1c153dec8..a3229fe1741 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -78,6 +78,18 @@ describe 'Admin updates settings' do expect(page).to have_content "Application settings saved successfully" end + it 'Change New users set to external', :js do + user_internal_regex = find('#application_setting_user_default_internal_regex', visible: :all) + + expect(user_internal_regex).to be_readonly + expect(user_internal_regex['placeholder']).to eq 'To define internal users, first enable new users set to external' + + check 'application_setting_user_default_external' + + expect(user_internal_regex).not_to be_readonly + expect(user_internal_regex['placeholder']).to eq 'Regex pattern' + end + it 'Change Sign-in restrictions' do page.within('.as-signin') do fill_in 'Home page URL', with: 'https://about.gitlab.com/' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index b2eaeb1c487..d32f33ca1e2 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -125,6 +125,52 @@ describe "Admin::Users" do expect(page).to have_content('Username can contain only letters, digits') end end + + context 'with new users set to external enabled' do + context 'with regex to match internal user email address set', :js do + before do + stub_application_setting(user_default_external: true) + stub_application_setting(user_default_internal_regex: '.internal@') + + visit new_admin_user_path + end + + def expects_external_to_be_checked + expect(find('#user_external')).to be_checked + end + + def expects_external_to_be_unchecked + expect(find('#user_external')).not_to be_checked + end + + def expects_warning_to_be_hidden + expect(find('#warning_external_automatically_set', visible: :all)[:class]).to include 'hidden' + end + + def expects_warning_to_be_shown + expect(find('#warning_external_automatically_set')[:class]).not_to include 'hidden' + end + + it 'automatically unchecks external for matching email' do + expects_external_to_be_checked + expects_warning_to_be_hidden + + fill_in 'user_email', with: 'test.internal@domain.ch' + + expects_external_to_be_unchecked + expects_warning_to_be_shown + + fill_in 'user_email', with: 'test@domain.ch' + + expects_external_to_be_checked + expects_warning_to_be_hidden + + uncheck 'user_external' + + expects_warning_to_be_hidden + end + end + end end describe "GET /admin/users/:id" do diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index b079802cb81..34d9115a1f6 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -42,6 +42,30 @@ describe UsersHelper do end end + describe '#user_internal_regex_data' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :user_default_internal_regex, :result) do + false | nil | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + false | '' | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + false | 'mockRegexPattern' | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + true | nil | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + true | '' | { user_internal_regex_pattern: nil, user_internal_regex_options: nil } + true | 'mockRegexPattern' | { user_internal_regex_pattern: 'mockRegexPattern', user_internal_regex_options: 'gi' } + end + + with_them do + before do + stub_application_setting(user_default_external: user_default_external) + stub_application_setting(user_default_internal_regex: user_default_internal_regex) + end + + subject { helper.user_internal_regex_data } + + it { is_expected.to eq(result) } + end + end + describe '#current_user_menu_items' do subject(:items) { helper.current_user_menu_items } diff --git a/spec/javascripts/fixtures/admin_users.rb b/spec/javascripts/fixtures/admin_users.rb new file mode 100644 index 00000000000..9989ac4fff2 --- /dev/null +++ b/spec/javascripts/fixtures/admin_users.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do + include StubENV + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + sign_in(admin) + end + + render_views + + before(:all) do + clean_frontend_fixtures('admin/users') + end + + it 'admin/users/new_with_internal_user_regex.html.raw' do |example| + stub_application_setting(user_default_external: true) + stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?') + + get :new + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/application_settings.rb b/spec/javascripts/fixtures/application_settings.rb new file mode 100644 index 00000000000..a9d3043f73d --- /dev/null +++ b/spec/javascripts/fixtures/application_settings.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :controller do + include StubENV + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + sign_in(admin) + end + + render_views + + before(:all) do + clean_frontend_fixtures('application_settings/') + end + + after do + remove_repository(project) + end + + it 'application_settings/accounts_and_limit.html.raw' do |example| + stub_application_setting(user_default_external: false) + + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js new file mode 100644 index 00000000000..4dbfd8f0eaa --- /dev/null +++ b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js @@ -0,0 +1,33 @@ +import $ from 'jquery'; +import initUserInternalRegexPlaceholder, { PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE, + PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE } from '~/pages/admin/application_settings/account_and_limits'; + +describe('AccountAndLimits', () => { + const FIXTURE = 'application_settings/accounts_and_limit.html.raw'; + let $userDefaultExternal; + let $userInternalRegex; + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + initUserInternalRegexPlaceholder(); + $userDefaultExternal = $('#application_setting_user_default_external'); + $userInternalRegex = document.querySelector('#application_setting_user_default_internal_regex'); + }); + + describe('Changing of userInternalRegex when userDefaultExternal', () => { + it('is unchecked', () => { + expect($userDefaultExternal.prop('checked')).toBeFalsy(); + expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE); + expect($userInternalRegex.readOnly).toBeTruthy(); + }); + + it('is checked', (done) => { + if (!$userDefaultExternal.prop('checked')) $userDefaultExternal.click(); + expect($userDefaultExternal.prop('checked')).toBeTruthy(); + expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE); + expect($userInternalRegex.readOnly).toBeFalsy(); + done(); + }); + }); +}); diff --git a/spec/javascripts/pages/admin/users/new/index_spec.js b/spec/javascripts/pages/admin/users/new/index_spec.js new file mode 100644 index 00000000000..2bac3951c3a --- /dev/null +++ b/spec/javascripts/pages/admin/users/new/index_spec.js @@ -0,0 +1,43 @@ +import $ from 'jquery'; +import UserInternalRegexHandler from '~/pages/admin/users/new/index'; + +describe('UserInternalRegexHandler', () => { + const FIXTURE = 'admin/users/new_with_internal_user_regex.html.raw'; + let $userExternal; + let $userEmail; + let $warningMessage; + + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + // eslint-disable-next-line no-new + new UserInternalRegexHandler(); + $userExternal = $('#user_external'); + $userEmail = $('#user_email'); + $warningMessage = $('#warning_external_automatically_set'); + if (!$userExternal.prop('checked')) $userExternal.prop('checked', 'checked'); + }); + + describe('Behaviour of userExternal checkbox when', () => { + it('matches email as internal', (done) => { + expect($warningMessage.hasClass('hidden')).toBeTruthy(); + + $userEmail.val('test@').trigger('input'); + + expect($userExternal.prop('checked')).toBeFalsy(); + expect($warningMessage.hasClass('hidden')).toBeFalsy(); + done(); + }); + + it('matches email as external', (done) => { + expect($warningMessage.hasClass('hidden')).toBeTruthy(); + + $userEmail.val('test.ext@').trigger('input'); + + expect($userExternal.prop('checked')).toBeTruthy(); + expect($warningMessage.hasClass('hidden')).toBeTruthy(); + done(); + }); + }); +}); diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 02f74e2ea54..483cc546423 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -538,4 +538,28 @@ describe ApplicationSetting do expect(setting.allow_signup?).to be_falsey end end + + describe '#user_default_internal_regex_enabled?' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :user_default_internal_regex, :result) do + false | nil | false + false | '' | false + false | '^(?:(?!\.ext@).)*$\r?\n?' | false + true | '' | false + true | nil | false + true | '^(?:(?!\.ext@).)*$\r?\n?' | true + end + + with_them do + before do + setting.update(user_default_external: user_default_external) + setting.update(user_default_internal_regex: user_default_internal_regex) + end + + subject { setting.user_default_internal_regex_enabled? } + + it { is_expected.to eq(result) } + end + end end diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb index 677d4a622e1..b987fe45138 100644 --- a/spec/services/users/build_service_spec.rb +++ b/spec/services/users/build_service_spec.rb @@ -13,6 +13,59 @@ describe Users::BuildService do it 'returns a valid user' do expect(service.execute).to be_valid end + + context 'with "user_default_external" application setting' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do + true | nil | 'fl@example.com' | nil | true + true | true | 'fl@example.com' | nil | true + true | false | 'fl@example.com' | nil | false + + true | nil | 'fl@example.com' | '' | true + true | true | 'fl@example.com' | '' | true + true | false | 'fl@example.com' | '' | false + + true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + + true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + + false | nil | 'fl@example.com' | nil | false + false | true | 'fl@example.com' | nil | true + false | false | 'fl@example.com' | nil | false + + false | nil | 'fl@example.com' | '' | false + false | true | 'fl@example.com' | '' | true + false | false | 'fl@example.com' | '' | false + + false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + + false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + end + + with_them do + before do + stub_application_setting(user_default_external: user_default_external) + stub_application_setting(user_default_internal_regex: user_default_internal_regex) + + params.merge!({ external: external, email: email }.compact) + end + + subject(:user) { service.execute } + + it 'correctly sets user.external' do + expect(user.external).to eq(result) + end + end + end end context 'with non admin user' do @@ -50,6 +103,59 @@ describe Users::BuildService do expect(service.execute).to be_confirmed end end + + context 'with "user_default_external" application setting' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do + true | nil | 'fl@example.com' | nil | true + true | true | 'fl@example.com' | nil | true + true | false | 'fl@example.com' | nil | true + + true | nil | 'fl@example.com' | '' | true + true | true | 'fl@example.com' | '' | true + true | false | 'fl@example.com' | '' | true + + true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + + true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + + false | nil | 'fl@example.com' | nil | false + false | true | 'fl@example.com' | nil | false + false | false | 'fl@example.com' | nil | false + + false | nil | 'fl@example.com' | '' | false + false | true | 'fl@example.com' | '' | false + false | false | 'fl@example.com' | '' | false + + false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + + false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + end + + with_them do + before do + stub_application_setting(user_default_external: user_default_external) + stub_application_setting(user_default_internal_regex: user_default_internal_regex) + + params.merge!({ external: external, email: email }.compact) + end + + subject(:user) { service.execute } + + it 'sets the value of Gitlab::CurrentSettings.user_default_external' do + expect(user.external).to eq(result) + end + end + end end end end diff --git a/spec/validators/js_regex_validator_spec.rb b/spec/validators/js_regex_validator_spec.rb new file mode 100644 index 00000000000..aeb55cdc0e5 --- /dev/null +++ b/spec/validators/js_regex_validator_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe JsRegexValidator do + describe '#validates_each' do + using RSpec::Parameterized::TableSyntax + + let(:validator) { described_class.new(attributes: [:user_default_internal_regex]) } + let(:application_setting) { build(:application_setting, user_default_external: true) } + + where(:user_default_internal_regex, :result) do + nil | [] + '' | [] + '(?#comment)' | ['Regex Pattern (?#comment) can not be expressed in Javascript'] + '(?(a)b|c)' | ['invalid conditional pattern: /(?(a)b|c)/i'] + '[a-z&&[^uo]]' | ["Dropped unsupported set intersection '[a-z&&[^uo]]' at index 0", + "Dropped unsupported nested negative set data '[^uo]' at index 6"] + end + + with_them do + it 'generates correct errors' do + validator.validate_each(application_setting, :user_default_internal_regex, user_default_internal_regex) + + expect(application_setting.errors[:user_default_internal_regex]).to eq result + end + end + end +end