Skip to content
Snippets Groups Projects
Commit 0a7775eb authored by Drew Blessing's avatar Drew Blessing :red_circle:
Browse files

Domain Verification MVC

Utilize a top-level group's verified project pages domains to
allow SCIM/SAML to create users without requiring email
confirmation. Also introduces a new top-level group setting page
to show project pages domains.

Changelog: added
EE: true
parent 8e7cc9ec
No related branches found
No related tags found
1 merge request!95407Domain Verification MVC
Showing
with 508 additions and 5 deletions
......@@ -58,6 +58,7 @@ class PagesDomain < ApplicationRecord
where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold))))
end
scope :verified, -> { where.not(verified_at: nil) }
scope :need_auto_ssl_renewal, -> do
enabled_and_not_failed = where(auto_ssl_enabled: true, auto_ssl_failed: false)
......
......@@ -16,3 +16,5 @@ def signup_params
end
end
end
Users::AuthorizedBuildService.prepend_mod_with('Users::AuthorizedBuildService')
# frozen_string_literal: true
module Groups
module Settings
class DomainVerificationController < Groups::ApplicationController
layout 'group_settings'
before_action :check_feature_availability
before_action :authorize_admin_group!
feature_category :authentication_and_authorization
urgency :low
def show
@hide_search_settings = true
@domains = group.all_projects_pages_domains(only_verified: false)
end
private
def check_feature_availability
render_404 unless group.domain_verification_available?
end
end
end
end
......@@ -475,6 +475,17 @@ def all_inherited_security_orchestration_policy_configurations
security_orchestration_policies_for_namespaces(ancestor_ids)
end
def all_projects_pages_domains(only_verified: false)
domains = ::PagesDomain.where(project_id: all_projects)
domains = domains.verified if only_verified
domains
end
def domain_verification_available?
::Gitlab.com? && root? && licensed_feature_available?(:domain_verification)
end
private
def security_orchestration_policies_for_namespaces(namespace_ids)
......
......@@ -98,6 +98,7 @@ class Features
default_branch_protection_restriction_in_groups
default_project_deletion_protection
disable_name_update_for_users
domain_verification
email_additional_text
epics
extended_audit_events
......
# frozen_string_literal: true
module EE
module Users
module AuthorizedBuildService
extend ::Gitlab::Utils::Override
PROVIDERS_ALLOWED_TO_SKIP_CONFIRMATION = [::Users::BuildService::GROUP_SCIM_PROVIDER,
::Users::BuildService::GROUP_SAML_PROVIDER].freeze
override :initialize
def initialize(current_user, params = nil)
super
set_skip_confirmation_param
end
private
def group
return unless params[:group_id]
strong_memoize(:group) do
::Group.find(params[:group_id])
end
end
def set_skip_confirmation_param
return if params[:skip_confirmation] # Explicit skip confirmation passed as param
return unless PROVIDERS_ALLOWED_TO_SKIP_CONFIRMATION.include?(params[:provider])
return unless group&.domain_verification_available?
verified_domains = group&.all_projects_pages_domains(only_verified: true)
return unless verified_domains.present? && params[:email] && ValidateEmail.valid?(params[:email])
email_domain = Mail::Address.new(params[:email]).domain.downcase
matches_verified_domain = verified_domains.map(&:domain).map(&:downcase).include?(email_domain)
params[:skip_confirmation] = true if matches_verified_domain
end
end
end
end
- page_title _('Domain Verification')
%h1.page-title.gl-font-size-h-display
= _('Domain Verification')
%p
= s_('DomainVerification|The following domains are configured for projects in this group. Users with email addresses that match a verified domain do not need to confirm their account.')
= link_to s_('DomainVerification|How do I configure a domain?'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
%table.gl-table.gl-w-full
%thead
%tr
%th= _('Domain')
%th= _('Project')
%tbody
- if @domains.empty?
%tr
%td{ colspan: 2, class: 'gl-py-6! text-center' }
= s_('DomainVerification|No domains configured. Create a domain in a project in this group hierarchy.')
- @domains.each do |domain|
%tr
%td.gl-lg-w-30p{ id: "domain#{domain.id}" }
= link_to domain.domain, project_pages_domain_path(domain.project, domain.domain)
- text, status = domain.unverified? ? [_('Unverified'), :danger] : [_('Verified'), :success]
= gl_badge_tag text, variant: status, size: :sm
%td
- project = domain.project
= link_to project.full_path, project_path(project)
......@@ -9,6 +9,7 @@
namespace :settings do
resource :reporting, only: [:show], controller: 'reporting'
resource :domain_verification, only: [:show], controller: 'domain_verification'
end
resources :group_members, only: [], concerns: :access_requestable do
......
......@@ -17,6 +17,7 @@ def configure_menu_items
add_item(ldap_sync_menu_item)
add_item(saml_sso_menu_item)
add_item(saml_group_links_menu_item)
add_item(domain_verification_menu_item)
add_item(usage_quotas_menu_item)
add_item(billing_menu_item)
add_item(reporting_menu_item)
......@@ -80,6 +81,21 @@ def saml_group_links_menu_item
)
end
def domain_verification_menu_item
return ::Sidebars::NilMenuItem.new(item_id: :domain_verification) unless domain_verification_available?
::Sidebars::MenuItem.new(
title: _('Domain Verification'),
link: group_settings_domain_verification_path(context.group),
active_routes: { path: 'domain_verification#show' },
item_id: :domain_verification
)
end
def domain_verification_available?
can?(context.current_user, :admin_group, context.group) && context.group.domain_verification_available?
end
def webhooks_menu_item
unless webhooks_enabled?
return ::Sidebars::NilMenuItem.new(item_id: :webhooks)
......
......@@ -71,6 +71,7 @@ def user_attributes
hash[:extern_uid] = auth_hash.uid
hash[:saml_provider_id] = @saml_provider.id
hash[:provider] = ::Users::BuildService::GROUP_SAML_PROVIDER
hash[:group_id] = saml_provider.group_id
end
end
......
......@@ -206,14 +206,14 @@
end
end
context 'for owners' do
context 'for owners', :saas do
before do
group.add_owner(user)
stub_group_wikis(false)
stub_feature_flags(harbor_registry_integration: false)
stub_feature_flags(observability_group_tab: false)
stub_licensed_features(domain_verification: true)
sign_in(user)
insert_package_nav(_('Kubernetes'))
end
......@@ -231,7 +231,7 @@
context 'when SAML SSO is available' do
before do
stub_licensed_features(group_saml: true)
stub_licensed_features(group_saml: true, domain_verification: true)
insert_after_nav_item(_('Security & Compliance'), new_nav_item: ci_cd_nav_item)
insert_after_nav_item(_('Analytics'), new_nav_item: settings_nav_item)
......@@ -266,7 +266,11 @@
end
before do
stub_licensed_features(security_dashboard: true, group_level_compliance_dashboard: true)
stub_licensed_features(
security_dashboard: true,
group_level_compliance_dashboard: true,
domain_verification: true
)
insert_after_nav_item(_('Security & Compliance'), new_nav_item: ci_cd_nav_item)
insert_after_nav_item(_('Analytics'), new_nav_item: settings_nav_item)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Group domain verification settings', :saas do
let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
before do
stub_licensed_features(domain_verification: true)
sign_in(user)
group.add_owner(user)
end
subject(:visit_domain_verification_page) { visit group_settings_domain_verification_path(group) }
it 'displays the side bar menu item' do
visit_domain_verification_page
page.within('.shortcuts-settings') do
expect(page).to have_link _('Domain Verification'), href: group_settings_domain_verification_path(group)
end
end
context 'when there are no domains' do
it 'displays no domains present message' do
visit_domain_verification_page
expect(page).to have_content s_('DomainVerification|No domains configured. Create a domain in a project in this group hierarchy.') # rubocop:disable Layout/LineLength
end
end
context 'when there are domains' do
let!(:project) { create(:project, group: group) }
let!(:verified_domain) { create(:pages_domain, project: project) }
let!(:unverified_domain) { create(:pages_domain, :unverified, project: project) }
it 'displays all domains within group hierarchy' do
visit_domain_verification_page
page.within("td#domain#{verified_domain.id}") do
expect(page).to have_link(verified_domain.domain,
href: project_pages_domain_path(project, verified_domain.domain))
expect(page).to have_selector '.badge', text: 'Verified'
end
page.within("td#domain#{unverified_domain.id}") do
expect(page).to have_link(unverified_domain.domain,
href: project_pages_domain_path(project, unverified_domain.domain))
expect(page).to have_selector '.badge', text: 'Unverified'
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do
RSpec.describe ::EE::Gitlab::Scim::ProvisioningService, :saas do
describe '#execute' do
let(:group) { create(:group) }
let(:service) { described_class.new(group, service_params) }
......@@ -103,6 +103,20 @@ def user
expect { service.execute }.to change { User.count }.by(1)
end
end
context 'when a verified pages domain matches the user email domain' do
before do
stub_licensed_features(domain_verification: true)
create(:pages_domain, project: create(:project, group: group), domain: 'example.com')
end
it 'creates a confirmed user' do
service.execute
expect(user).to be_present
expect(user).to be_confirmed
end
end
end
context 'invalid params' do
......
......@@ -109,6 +109,32 @@
end
end
describe 'domain verification', :saas do
let(:item_id) { :domain_verification }
context 'when domain verification is licensed' do
before do
stub_licensed_features(domain_verification: true)
end
it { is_expected.to be_present }
context 'when user cannot admin group' do
let(:user) { nil }
it { is_expected.to be_nil }
end
end
context 'when domain verification is not licensed' do
before do
stub_licensed_features(domain_verification: false)
end
it { is_expected.to be_nil }
end
end
describe 'Webhooks menu' do
let(:item_id) { :webhooks }
let(:group_webhooks_enabled) { true }
......
......@@ -112,6 +112,17 @@ def create_existing_identity
expect { find_and_update }.to change { Identity.count }.by(1)
end
context 'when a verified pages domain matches the user email domain', :saas do
before do
stub_licensed_features(domain_verification: true)
create(:pages_domain, project: create(:project, group: group), domain: info_hash[:email].split('@')[1])
end
it 'confirms the user' do
expect(find_and_update).to be_confirmed
end
end
context 'when user attributes are present' do
before do
auth_hash[:extra][:raw_info] =
......
......@@ -1733,6 +1733,80 @@
end
end
describe '#all_projects_pages_domains' do
let_it_be(:namespace) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: namespace) }
let_it_be(:project1) { create(:project, group: namespace) }
let_it_be(:project2) { create(:project, group: subgroup) }
let!(:verified_domain) { create(:pages_domain, project: project1) }
let!(:unverified_domain) { create(:pages_domain, :unverified, project: project2) }
it 'finds all pages domains by default' do
expect(namespace.all_projects_pages_domains).to match_array([verified_domain, unverified_domain])
end
it 'finds only verified domains when param is true' do
expect(namespace.all_projects_pages_domains(only_verified: true)).to match_array(verified_domain)
end
context 'when projects are outside the top-level group hierarchy' do
before do
outside_namespace = create(:group)
outside_project = create(:project, group: outside_namespace)
create(:pages_domain, project: outside_project)
end
it 'does not include the outside domain' do
expect(namespace.all_projects_pages_domains).to match_array([verified_domain, unverified_domain])
end
end
end
describe '#domain_verification_available?' do
let(:namespace) { create(:group) }
context 'when the feature is not licensed' do
before do
stub_licensed_features(domain_verification: false)
end
it 'is not available' do
expect(namespace.domain_verification_available?).to eq(false)
end
context 'on GitLab.com', :saas do
it 'is not available' do
expect(namespace.domain_verification_available?).to eq(false)
end
end
end
context 'when the feature is licensed' do
before do
stub_licensed_features(domain_verification: true)
end
it 'is not available' do
expect(namespace.domain_verification_available?).to eq(false)
end
context 'on GitLab.com', :saas do
it 'is available' do
expect(namespace.domain_verification_available?).to eq(true)
end
context 'with a subgroup' do
let(:subgroup) { create(:group, :nested) }
it 'is not available' do
expect(subgroup.domain_verification_available?).to eq(false)
end
end
end
end
end
describe '#allow_stale_runner_pruning?' do
subject { namespace.allow_stale_runner_pruning? }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::Settings::DomainVerificationController, type: :request do
shared_examples 'renders 404' do
it 'renders 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'GET /groups/:group_id/-/settings/domain_verification', :saas do
let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
subject(:perform_request) { get group_settings_domain_verification_path(group) }
before do
stub_licensed_features(domain_verification: licensed_feature_available)
group.add_member(user, access_level)
sign_in(user)
perform_request
end
context 'when the feature is available' do
let(:licensed_feature_available) { true }
context 'when the user is an owner' do
let(:access_level) { :owner }
it 'renders show with 200 status code' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
context 'when subgroup' do
let(:group) { create(:group, parent: create(:group)) }
it_behaves_like 'renders 404'
end
end
context 'when user is not owner' do
let(:access_level) { :maintainer }
it_behaves_like 'renders 404'
end
end
context 'when domain verification is unavailable' do
let(:licensed_feature_available) { false }
let(:access_level) { :owner }
it_behaves_like 'renders 404'
end
end
end
......@@ -27,5 +27,112 @@
end
end
end
context 'with a nil user' do
let(:service) { described_class.new(nil, params) }
let(:params) do
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
end
shared_examples 'unsuccessful user domain matching' do
it 'creates an unconfirmed user' do
user = service.execute
expect(user).to be_present
expect(user).not_to be_confirmed
end
end
shared_examples 'successful user domain matching' do
it 'creates a confirmed user' do
user = service.execute
expect(user).to be_present
expect(user).to be_confirmed
end
end
context 'when user confirmation is enabled' do
before do
stub_application_setting(send_user_confirmation_email: true)
end
it_behaves_like 'unsuccessful user domain matching'
context 'with a group_id param' do
let(:group) { create(:group) }
before do
params[:group_id] = group.id
end
it_behaves_like 'unsuccessful user domain matching'
context 'when domain verification is available', :saas do
before do
stub_licensed_features(domain_verification: true)
end
context 'when a matching verified domain is present in the group hierarchy' do
before do
project = create(:project, group: group)
create(:pages_domain, project: project, domain: 'example.com')
end
using RSpec::Parameterized::TableSyntax
where(:provider, :expect_confirmed?) do
::Users::BuildService::GROUP_SAML_PROVIDER | true
::Users::BuildService::GROUP_SCIM_PROVIDER | true
'google_oauth2' | false
end
with_them do
it 'creates a user with proper confirmation' do
params[:provider] = provider
user = service.execute
expect(user).to be_present
expect(user.confirmed?).to eq(expect_confirmed?)
end
end
context 'with various email address formats' do
before do
params[:provider] = ::Users::BuildService::GROUP_SAML_PROVIDER
end
context 'when email domain case varies' do
before do
params[:email] = 'jdoe@EXample.com'
end
it_behaves_like 'successful user domain matching'
end
context 'when the email address is invalid' do
before do
params[:email] = 'jdoe@jdoe@example.com'
end
it_behaves_like 'unsuccessful user domain matching'
end
end
end
context 'when no verified domains exist' do
before do
project = create(:project, group: group)
create(:pages_domain, :unverified, project: project, domain: 'example.com')
params[:provider] = ::Users::BuildService::GROUP_SAML_PROVIDER
end
it_behaves_like 'unsuccessful user domain matching'
end
end
end
end
end
end
end
......@@ -13980,6 +13980,18 @@ msgstr ""
msgid "Domain Name"
msgstr ""
 
msgid "Domain Verification"
msgstr ""
msgid "DomainVerification|How do I configure a domain?"
msgstr ""
msgid "DomainVerification|No domains configured. Create a domain in a project in this group hierarchy."
msgstr ""
msgid "DomainVerification|The following domains are configured for projects in this group. Users with email addresses that match a verified domain do not need to confirm their account."
msgstr ""
msgid "Don't have a group?"
msgstr ""
 
......@@ -21,6 +21,15 @@
end
end
describe '.verified' do
let!(:verified) { create(:pages_domain) }
let!(:unverified) { create(:pages_domain, :unverified) }
it 'finds verified' do
expect(described_class.verified).to match_array(verified)
end
end
describe 'validate domain' do
subject(:pages_domain) { build(:pages_domain, domain: domain) }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment