Commit 95bf33d5 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'jej/group-saml-unlink-from-account' into 'master'

Users can unlink Group SAML from accounts page

Closes #5016

See merge request gitlab-org/gitlab-ee!8682
parents d01cb0a8 be318259
...@@ -144,11 +144,13 @@ ...@@ -144,11 +144,13 @@
.provider-btn-group { .provider-btn-group {
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
margin-bottom: 10px;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: 3px; border-radius: 3px;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
margin-bottom: 0;
} }
} }
......
...@@ -4,7 +4,7 @@ class Profiles::AccountsController < Profiles::ApplicationController ...@@ -4,7 +4,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper include AuthHelper
def show def show
@user = current_user render(locals: show_view_variables)
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -23,4 +23,12 @@ class Profiles::AccountsController < Profiles::ApplicationController ...@@ -23,4 +23,12 @@ class Profiles::AccountsController < Profiles::ApplicationController
redirect_to profile_account_path redirect_to profile_account_path
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private
def show_view_variables
{}
end
end end
Profiles::AccountsController.prepend(EE::Profiles::AccountsController)
...@@ -57,6 +57,10 @@ module AuthHelper ...@@ -57,6 +57,10 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) } auth_providers.reject { |provider| form_based_provider?(provider) }
end end
def display_providers_on_profile?
button_based_providers.any?
end
def providers_for_base_controller def providers_for_base_controller
auth_providers.reject { |provider| LDAP_PROVIDER === provider } auth_providers.reject { |provider| LDAP_PROVIDER === provider }
end end
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success' = link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success'
%hr %hr
- if button_based_providers.any? - if display_providers_on_profile?
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
- else - else
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect Connect
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities]
%hr %hr
- if current_user.can_change_username? - if current_user.can_change_username?
.row.prepend-top-default .row.prepend-top-default
...@@ -66,7 +67,7 @@ ...@@ -66,7 +67,7 @@
%h4.prepend-top-0.danger-title %h4.prepend-top-0.danger-title
= s_('Profiles|Delete account') = s_('Profiles|Delete account')
.col-lg-8 .col-lg-8
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if current_user.can_be_removed? && can?(current_user, :destroy_user, current_user)
%p %p
= s_('Profiles|Deleting an account has the following effects:') = s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user = render 'users/deletion_guidance', user: current_user
...@@ -79,10 +80,10 @@ ...@@ -79,10 +80,10 @@
confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
username: current_user.username } } username: current_user.username } }
- else - else
- if @user.solo_owned_groups.present? - if current_user.solo_owned_groups.present?
%p %p
= s_('Profiles|Your account is currently an owner in these groups:') = s_('Profiles|Your account is currently an owner in these groups:')
%strong= @user.solo_owned_groups.map(&:name).join(', ') %strong= current_user.solo_owned_groups.map(&:name).join(', ')
%p %p
= s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.') = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.')
- else - else
......
# frozen_string_literal: true
module EE
module Profiles::AccountsController
extend ::Gitlab::Utils::Override
private
override :show_view_variables
def show_view_variables
group_saml_identities = GroupSamlIdentityFinder.new(user: current_user).all
super.merge(group_saml_identities: group_saml_identities)
end
end
end
...@@ -6,7 +6,8 @@ class Groups::SsoController < Groups::ApplicationController ...@@ -6,7 +6,8 @@ class Groups::SsoController < Groups::ApplicationController
before_action :check_group_saml_configured before_action :check_group_saml_configured
before_action :check_group_saml_available! before_action :check_group_saml_available!
before_action :require_configured_provider before_action :require_configured_provider
before_action :check_user_can_sign_in_with_provider before_action :authenticate_user!, only: [:unlink]
before_action :check_user_can_sign_in_with_provider, only: [:saml]
before_action :redirect_if_group_moved before_action :redirect_if_group_moved
layout 'devise' layout 'devise'
...@@ -16,8 +17,20 @@ class Groups::SsoController < Groups::ApplicationController ...@@ -16,8 +17,20 @@ class Groups::SsoController < Groups::ApplicationController
@group_name = @unauthenticated_group.full_name @group_name = @unauthenticated_group.full_name
end end
def unlink
return route_not_found unless linked_identity
GroupSaml::Identity::DestroyService.new(linked_identity).execute
redirect_to profile_account_path
end
private private
def linked_identity
@linked_identity ||= GroupSamlIdentityFinder.new(user: current_user).find_linked(group: @unauthenticated_group)
end
def check_group_saml_available! def check_group_saml_available!
route_not_found unless @unauthenticated_group.feature_available?(:group_saml) route_not_found unless @unauthenticated_group.feature_available?(:group_saml)
end end
......
# frozen_string_literal: true
class GroupSamlIdentityFinder
attr_reader :user
def initialize(user:)
@user = user
end
def find_linked(group:)
return unless user
group&.saml_provider&.identities&.find_by(user: user)
end
def all
user.group_saml_identities.preload_saml_group
end
end
...@@ -8,6 +8,11 @@ module EE ...@@ -8,6 +8,11 @@ module EE
delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings' delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings'
override :display_providers_on_profile?
def display_providers_on_profile?
super || group_saml_enabled?
end
override :button_based_providers override :button_based_providers
def button_based_providers def button_based_providers
super - GROUP_LEVEL_PROVIDERS super - GROUP_LEVEL_PROVIDERS
...@@ -45,6 +50,10 @@ module EE ...@@ -45,6 +50,10 @@ module EE
::Gitlab::Auth::Smartcard.enabled? ::Gitlab::Auth::Smartcard.enabled?
end end
def group_saml_enabled?
auth_providers.include?(:group_saml)
end
def slack_redirect_uri(project) def slack_redirect_uri(project)
slack_auth_project_settings_slack_url(project) slack_auth_project_settings_slack_url(project)
end end
......
...@@ -18,5 +18,11 @@ module EE ...@@ -18,5 +18,11 @@ module EE
iwhere(secondary_extern_uid: normalize_uid(provider, secondary_extern_uid)).with_provider(provider) iwhere(secondary_extern_uid: normalize_uid(provider, secondary_extern_uid)).with_provider(provider)
end end
end end
class_methods do
def preload_saml_group
preload(saml_provider: { group: :route })
end
end
end end
end end
# frozen_string_literal: true
module GroupSaml
module Identity
class DestroyService
attr_reader :identity
delegate :user, to: :identity
def initialize(identity)
@identity = identity
end
def execute
identity.destroy!
remove_group_access
end
private
def remove_group_access
return unless group_membership
return if group.last_owner?(user)
Members::DestroyService.new(user).execute(group_membership)
end
def group
@group ||= identity.saml_provider.group
end
def group_membership
@group_membership ||= group.group_member(user)
end
end
end
end
- group_saml_identities.each do |identity|
- group = identity.saml_provider.group
.provider-btn-group
.provider-btn-image
= _("SAML for %{group_name}") % { group_name: group.name }
= link_to unlink_group_saml_providers_path(group), method: :delete, class: 'provider-btn' do
Disconnect
---
title: Users can unlink Group SAML from accounts page
merge_request: 8682
author:
type: changed
...@@ -77,6 +77,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -77,6 +77,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :saml_providers, path: 'saml', only: [:show, :create, :update] do resource :saml_providers, path: 'saml', only: [:show, :create, :update] do
post :callback, to: 'omniauth_callbacks#group_saml' post :callback, to: 'omniauth_callbacks#group_saml'
get :sso, to: 'sso#saml' get :sso, to: 'sso#saml'
delete :unlink, to: 'sso#unlink'
end end
resource :roadmap, only: [:show], controller: 'roadmap' resource :roadmap, only: [:show], controller: 'roadmap'
......
# frozen_string_literal: true
FactoryBot.define do
factory :group_saml_identity, class: Identity, parent: :identity do
provider 'group_saml'
extern_uid { generate(:username) }
saml_provider
user
end
end
# frozen_string_literal: true
require 'rails_helper'
describe 'Profile > Account' do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe "Disconnect Group SAML", :js do
let(:group) { create(:group, :private, name: 'Test Group') }
let(:saml_provider) { create(:saml_provider, group: group) }
def enable_group_saml
stub_licensed_features(group_saml: true)
allow(Devise).to receive(:omniauth_providers).and_return(%i(group_saml))
end
def create_linked_identity
oauth = { 'provider' => 'group_saml', 'uid' => '1' }
Gitlab::Auth::GroupSaml::IdentityLinker.new(user, oauth, saml_provider).link
end
before do
enable_group_saml
create_linked_identity
end
it 'unlinks account' do
visit profile_account_path
unlink_label = "SAML for Test Group"
expect(page).to have_content unlink_label
click_link "Disconnect"
expect(current_path).to eq profile_account_path
expect(page).not_to have_content(unlink_label)
visit group_path(group)
expect(page).to have_content('Page Not Found')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GroupSamlIdentityFinder do
include Gitlab::Routing
let(:user) { create(:user) }
let!(:identity) { create(:group_saml_identity, user: user) }
let(:group) { identity.saml_provider.group }
subject { described_class.new(user: user) }
describe "#find_linked" do
it "finds identity matching user and group" do
expect(subject.find_linked(group: group)).to eq(identity)
end
it "returns nil when no saml_provider exists" do
group.saml_provider.destroy!
expect(subject.find_linked(group: group)).to eq(nil)
end
it "returns nil when group is nil" do
expect(subject.find_linked(group: nil)).to eq(nil)
end
end
describe "#all" do
it "finds Group SAML identities for a user" do
expect(subject.all.first).to eq(identity)
end
it "avoids N+1 on access to provider and group path" do
identity = subject.all.first
expect { group_path(identity.saml_provider.group) }.not_to exceed_query_limit(0)
end
end
end
...@@ -14,12 +14,12 @@ describe EE::MembersPreloader do ...@@ -14,12 +14,12 @@ describe EE::MembersPreloader do
it 'preloads SAML identities to avoid N+1 queries in MembersPresenter' do it 'preloads SAML identities to avoid N+1 queries in MembersPresenter' do
member = create(:group_member, group: group) member = create(:group_member, group: group)
create(:identity, :group_saml, user: member.user, saml_provider: saml_provider) create(:group_saml_identity, user: member.user, saml_provider: saml_provider)
control = ActiveRecord::QueryRecorder.new { group_sso_with_preload([member]) } control = ActiveRecord::QueryRecorder.new { group_sso_with_preload([member]) }
members = create_list(:group_member, 3, group: group) members = create_list(:group_member, 3, group: group)
create(:identity, :group_saml, user: members.first.user, saml_provider: saml_provider) create(:group_saml_identity, user: members.first.user, saml_provider: saml_provider)
create(:identity, :group_saml, user: members.last.user, saml_provider: saml_provider) create(:group_saml_identity, user: members.last.user, saml_provider: saml_provider)
expect { group_sso_with_preload(members) }.not_to exceed_query_limit(control) expect { group_sso_with_preload(members) }.not_to exceed_query_limit(control)
end end
......
...@@ -257,7 +257,7 @@ describe EE::User do ...@@ -257,7 +257,7 @@ describe EE::User do
end end
context 'with linked identity' do context 'with linked identity' do
let!(:identity) { create(:identity, :group_saml, user: user) } let!(:identity) { create(:group_saml_identity, user: user) }
let(:saml_provider) { identity.saml_provider } let(:saml_provider) { identity.saml_provider }
let(:group) { saml_provider.group } let(:group) { saml_provider.group }
...@@ -267,7 +267,7 @@ describe EE::User do ...@@ -267,7 +267,7 @@ describe EE::User do
end end
it 'does not cause ActiveRecord to loop through identites' do it 'does not cause ActiveRecord to loop through identites' do
create(:identity, :group_saml, user: user) create(:group_saml_identity, user: user)
expect(Identity).not_to receive(:instantiate) expect(Identity).not_to receive(:instantiate)
......
# frozen_string_literal: true
require 'spec_helper'
describe GroupSaml::Identity::DestroyService do
let(:identity) { create(:group_saml_identity) }
subject { described_class.new(identity) }
before do
link_group_membership
end
def link_group_membership
Gitlab::Auth::GroupSaml::MembershipUpdater.new(identity.user, identity.saml_provider).execute
end
it "prevents future Group SAML logins" do
subject.execute
expect(identity).to be_destroyed
end
it "removes access to the group" do
expect do
subject.execute
end.to change(GroupMember, :count).by(-1)
end
it "doesn't remove the last group owner" do
identity.saml_provider.group.members.first.update!(access_level: Gitlab::Access::OWNER)
expect do
subject.execute
end.not_to change(GroupMember, :count)
end
it 'logs an audit event' do
expect do
subject.execute
end.to change { SecurityEvent.count }.by(1)
end
end
...@@ -7295,6 +7295,9 @@ msgstr "" ...@@ -7295,6 +7295,9 @@ msgstr ""
msgid "SAML Single Sign On Settings" msgid "SAML Single Sign On Settings"
msgstr "" msgstr ""
msgid "SAML for %{group_name}"
msgstr ""
msgid "SAST" msgid "SAST"
msgstr "" msgstr ""
......
...@@ -2,11 +2,5 @@ FactoryBot.define do ...@@ -2,11 +2,5 @@ FactoryBot.define do
factory :identity do factory :identity do
provider 'ldapmain' provider 'ldapmain'
extern_uid 'my-ldap-id' extern_uid 'my-ldap-id'
trait :group_saml do
provider 'group_saml'
extern_uid { generate(:username) }
saml_provider
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment