Commit 90bde7e1 authored by Drew Blessing's avatar Drew Blessing Committed by Drew Blessing

Allow Group SAML to auto-created new users

If a user does not already exist, allow Group SAML
sign-in process to automatically create the user. This enables
just-in-time provisioning via Group SAML.
parent 032a105a
...@@ -194,11 +194,13 @@ If the information you need isn't listed above you may wish to check our [troubl ...@@ -194,11 +194,13 @@ If the information you need isn't listed above you may wish to check our [troubl
Once Group SSO is configured and enabled, users can access the GitLab.com group through the identity provider's dashboard. If [SCIM](scim_setup.md) is configured, please see the [user access and linking setup section on the SCIM page](scim_setup.md#user-access-and-linking-setup). Once Group SSO is configured and enabled, users can access the GitLab.com group through the identity provider's dashboard. If [SCIM](scim_setup.md) is configured, please see the [user access and linking setup section on the SCIM page](scim_setup.md#user-access-and-linking-setup).
When a user tries to sign in with Group SSO, they need an account that's configured with one of the following: When a user tries to sign in with Group SSO, GitLab attempts to find or create a user based on the following:
- [SCIM](scim_setup.md). - Find an existing user with a matching SAML identity. This would mean the user either had their account created by [SCIM](scim_setup.md) or they have previously signed in with the group's SAML IdP.
- [Group-managed accounts](group_managed_accounts.md). - If there is no conflicting user with the same email address, create a new account automatically.
- A GitLab.com account. - If there is a conflicting user with the same email address, redirect the user to the sign-in page to:
- Create a new account with another email address.
- Sign-in to their existing account to link the SAML identity.
### Linking SAML to your existing GitLab.com account ### Linking SAML to your existing GitLab.com account
......
...@@ -95,7 +95,9 @@ class Groups::OmniauthCallbacksController < OmniauthCallbacksController ...@@ -95,7 +95,9 @@ class Groups::OmniauthCallbacksController < OmniauthCallbacksController
override :build_auth_user override :build_auth_user
def build_auth_user(auth_user_class) def build_auth_user(auth_user_class)
Gitlab::Auth::GroupSaml::User.new(oauth, @saml_provider) super.tap do |auth_user|
auth_user.saml_provider = @saml_provider
end
end end
override :fail_login override :fail_login
......
---
title: Allow Group SAML to auto-created new users
merge_request: 48953
author:
type: added
...@@ -31,7 +31,8 @@ module Gitlab ...@@ -31,7 +31,8 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def update_group_membership def update_group_membership
MembershipUpdater.new(current_user, saml_provider, oauth).execute auth_hash = AuthHash.new(oauth)
MembershipUpdater.new(current_user, saml_provider, auth_hash).execute
end end
end end
end end
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
def initialize(user, saml_provider, auth_hash) def initialize(user, saml_provider, auth_hash)
@user = user @user = user
@saml_provider = saml_provider @saml_provider = saml_provider
@auth_hash = AuthHash.new(auth_hash) @auth_hash = auth_hash
end end
def execute def execute
...@@ -54,7 +54,7 @@ module Gitlab ...@@ -54,7 +54,7 @@ module Gitlab
def group_names_from_saml def group_names_from_saml
strong_memoize(:group_names_from_saml) do strong_memoize(:group_names_from_saml) do
auth_hash.groups auth_hash.groups || []
end end
end end
......
...@@ -3,42 +3,73 @@ ...@@ -3,42 +3,73 @@
module Gitlab module Gitlab
module Auth module Auth
module GroupSaml module GroupSaml
class User class User < Gitlab::Auth::OAuth::User
attr_reader :auth_hash, :saml_provider include ::Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
def initialize(auth_hash, saml_provider) attr_accessor :saml_provider
@auth_hash = auth_hash attr_reader :auth_hash
@saml_provider = saml_provider
override :initialize
def initialize(auth_hash)
@auth_hash = AuthHash.new(auth_hash)
end end
override :find_and_update!
def find_and_update! def find_and_update!
update_group_membership save("GroupSaml Provider ##{saml_provider.id}")
user_from_identity # Do not return un-persisted user so user is prompted
end # to sign-in to existing account.
return unless valid_sign_in?
def valid_sign_in? update_group_membership
user_from_identity.present? gl_user
end end
override :bypass_two_factor?
def bypass_two_factor? def bypass_two_factor?
false false
end end
private private
override :gl_user
def gl_user
strong_memoize(:gl_user) do
identity&.user || build_new_user
end
end
def identity def identity
@identity ||= ::Auth::GroupSamlIdentityFinder.new(saml_provider, auth_hash).first strong_memoize(:identity) do
::Auth::GroupSamlIdentityFinder.new(saml_provider, auth_hash).first
end
end
override :build_new_user
def build_new_user(skip_confirmation: false)
super.tap do |user|
user.provisioned_by_group_id = saml_provider.group_id
end
end end
def user_from_identity override :user_attributes
@user_from_identity ||= identity&.user def user_attributes
super.tap do |hash|
hash[:extern_uid] = auth_hash.uid
hash[:saml_provider_id] = @saml_provider.id
hash[:provider] = ::Users::BuildService::GROUP_SAML_PROVIDER
end
end end
def update_group_membership def update_group_membership
return unless user_from_identity MembershipUpdater.new(gl_user, saml_provider, auth_hash).execute
end
MembershipUpdater.new(user_from_identity, saml_provider, auth_hash).execute override :block_after_signup?
def block_after_signup?
false
end end
end end
end end
......
...@@ -6,13 +6,15 @@ RSpec.describe Gitlab::Auth::GroupSaml::MembershipUpdater do ...@@ -6,13 +6,15 @@ RSpec.describe Gitlab::Auth::GroupSaml::MembershipUpdater do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:saml_provider) { create(:saml_provider, default_membership_role: Gitlab::Access::DEVELOPER) } let(:saml_provider) { create(:saml_provider, default_membership_role: Gitlab::Access::DEVELOPER) }
let(:group) { saml_provider.group } let(:group) { saml_provider.group }
let(:omniauth_auth_hash) do let(:auth_hash) do
OmniAuth::AuthHash.new(extra: { Gitlab::Auth::GroupSaml::AuthHash.new(
raw_info: OneLogin::RubySaml::Attributes.new('groups' => %w(Developers Owners)) OmniAuth::AuthHash.new(extra: {
}) raw_info: OneLogin::RubySaml::Attributes.new('groups' => %w(Developers Owners))
})
)
end end
subject(:update_membership) { described_class.new(user, saml_provider, omniauth_auth_hash).execute } subject(:update_membership) { described_class.new(user, saml_provider, auth_hash).execute }
it 'adds the user to the group' do it 'adds the user to the group' do
subject subject
......
...@@ -5,11 +5,22 @@ require 'spec_helper' ...@@ -5,11 +5,22 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth::GroupSaml::User do RSpec.describe Gitlab::Auth::GroupSaml::User do
let(:uid) { 1234 } let(:uid) { 1234 }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid) }
let(:saml_provider) { create(:saml_provider) } let(:saml_provider) { create(:saml_provider) }
let(:group) { saml_provider.group } let(:group) { saml_provider.group }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: 'group_saml', info: info_hash) }
let(:info_hash) do
{
name: generate(:name),
email: generate(:email)
}
end
subject(:oauth_user) do
oauth_user = described_class.new(auth_hash)
oauth_user.saml_provider = saml_provider
subject { described_class.new(auth_hash, saml_provider) } oauth_user
end
def create_existing_identity def create_existing_identity
create(:group_saml_identity, extern_uid: uid, saml_provider: saml_provider) create(:group_saml_identity, extern_uid: uid, saml_provider: saml_provider)
...@@ -32,24 +43,59 @@ RSpec.describe Gitlab::Auth::GroupSaml::User do ...@@ -32,24 +43,59 @@ RSpec.describe Gitlab::Auth::GroupSaml::User do
end end
describe '#find_and_update!' do describe '#find_and_update!' do
subject(:find_and_update) { oauth_user.find_and_update! }
context 'with matching user for that group and uid' do context 'with matching user for that group and uid' do
let!(:identity) { create_existing_identity } let!(:identity) { create_existing_identity }
it 'updates group membership' do it 'updates group membership' do
expect do expect { find_and_update }.to change { group.members.count }.by(1)
subject.find_and_update!
end.to change { group.members.count }.by(1)
end end
it 'returns the user' do it 'returns the user' do
expect(subject.find_and_update!).to eq identity.user expect(find_and_update).to eq identity.user
end
it 'does not mark the user as provisioned' do
expect(find_and_update.provisioned_by_group).to be_nil
end end
end end
context 'with no matching user identity' do context 'with no matching user identity' do
it 'does nothing' do context 'when a user does not exist' do
expect(subject.find_and_update!).to eq nil it 'creates the user' do
expect(group.members.count).to eq 0 expect { find_and_update }.to change { User.count }.by(1)
end
it 'does not confirm the user' do
is_expected.not_to be_confirmed
end
it 'returns the correct user' do
expect(find_and_update.email).to eq info_hash[:email]
end
it 'marks the user as provisioned by the group' do
expect(find_and_update.provisioned_by_group).to eq group
end
it 'creates the user SAML identity' do
expect { find_and_update }.to change { Identity.count }.by(1)
end
end
context 'when a conflicting user already exists' do
before do
create(:user, email: info_hash[:email])
end
it 'does not update membership' do
expect { find_and_update }.not_to change { group.members.count }
end
it 'does not return a user' do
expect(find_and_update).to eq nil
end
end end
end end
end end
......
...@@ -197,8 +197,8 @@ module Gitlab ...@@ -197,8 +197,8 @@ module Gitlab
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def build_new_user def build_new_user(skip_confirmation: true)
user_params = user_attributes.merge(skip_confirmation: true) user_params = user_attributes.merge(skip_confirmation: skip_confirmation)
Users::BuildService.new(nil, user_params).execute(skip_authorization: true) Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
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