Commit 52c3dfde authored by Vasilii Iakliushin's avatar Vasilii Iakliushin

Merge branch 'support_multiple_saml_providers' into 'master'

Support external authentification of users through multiple SAML providers

See merge request gitlab-org/gitlab!73539
parents 8ab63783 e7568f5a
...@@ -9,7 +9,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -9,7 +9,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
after_action :verify_known_sign_in after_action :verify_known_sign_in
protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true protect_from_forgery except: [:kerberos, :saml, :cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true
feature_category :authentication_and_authorization feature_category :authentication_and_authorization
......
...@@ -86,6 +86,17 @@ module AuthHelper ...@@ -86,6 +86,17 @@ module AuthHelper
auth_providers.select { |provider| form_based_provider?(provider) } auth_providers.select { |provider| form_based_provider?(provider) }
end end
def saml_providers
auth_providers.select { |provider| auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML' }
end
def auth_strategy_class(provider)
config = Gitlab::Auth::OAuth::Provider.config_for(provider)
return if config.nil? || config['args'].blank?
config.args['strategy_class']
end
def any_form_based_providers_enabled? def any_form_based_providers_enabled?
form_based_providers.any? { |provider| form_enabled_for_sign_in?(provider) } form_based_providers.any? { |provider| form_enabled_for_sign_in?(provider) }
end end
......
...@@ -6,11 +6,13 @@ ...@@ -6,11 +6,13 @@
- providers.each do |provider| - providers.each do |provider|
- unlink_allowed = unlink_provider_allowed?(provider) - unlink_allowed = unlink_provider_allowed?(provider)
- link_allowed = link_provider_allowed?(provider) - link_allowed = link_provider_allowed?(provider)
- has_icon = provider_has_icon?(provider)
- if unlink_allowed || link_allowed - if unlink_allowed || link_allowed
- if auth_active?(provider) - if auth_active?(provider)
- if unlink_allowed - if unlink_allowed
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: button_class do = link_to unlink_profile_account_path(provider: provider), method: :delete, class: button_class do
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider) - if has_icon
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
.gl-button-text .gl-button-text
= s_('Profiles|Disconnect %{provider}') % { provider: label_for_provider(provider) } = s_('Profiles|Disconnect %{provider}') % { provider: label_for_provider(provider) }
- else - else
...@@ -19,7 +21,8 @@ ...@@ -19,7 +21,8 @@
= s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) } = s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) }
- elsif link_allowed - elsif link_allowed
= link_to omniauth_authorize_path(:user, provider), method: :post, class: button_class do = link_to omniauth_authorize_path(:user, provider), method: :post, class: button_class do
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider) - if has_icon
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
.gl-button-text .gl-button-text
= s_('Profiles|Connect %{provider}') % { provider: label_for_provider(provider) } = s_('Profiles|Connect %{provider}') % { provider: label_for_provider(provider) }
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities
...@@ -163,6 +163,74 @@ On the sign in page there should now be a SAML button below the regular sign in ...@@ -163,6 +163,74 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user Click the icon to begin the authentication process. If everything goes well the user
is returned to GitLab and signed in. is returned to GitLab and signed in.
### Use multiple SAML identity providers
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14361) in GitLab 14.6.
You can configure GitLab to use multiple SAML identity providers if:
- Each provider has a unique name set that matches a name set in `args`.
- The providers' names are:
- Used in OmniAuth configuration for properties based on the provider name. For example, `allowBypassTwoFactor`, `allowSingleSignOn`, and
`syncProfileFromProvider`.
- Used for association to each existing user as an additional identity.
- The `assertion_consumer_service_url` matches the provider name.
- The `strategy_class` is explicitly set because it cannot be inferred from provider name.
Example multiple providers configuration for Omnibus GitLab:
```ruby
gitlab_rails['omniauth_providers'] = [
{
name: 'saml_1',
args: {
name: 'saml_1', # This is mandatory and must match the provider name
strategy_class: 'OmniAuth::Strategies::SAML'
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
... # Put here all the required arguments similar to a single provider
},
label: 'Provider 1' # Differentiate the two buttons and providers in the UI
},
{
name: 'saml_2',
args: {
name: 'saml_2', # This is mandatory and must match the provider name
strategy_class: 'OmniAuth::Strategies::SAML'
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
... # Put here all the required arguments similar to a single provider
},
label: 'Provider 2' # Differentiate the two buttons and providers in the UI
}
]
```
Example providers configuration for installations from source:
```yaml
omniauth:
providers:
- {
name: 'saml_1',
args: {
name: 'saml_1', # This is mandatory and must match the provider name
strategy_class: 'OmniAuth::Strategies::SAML',
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
... # Put here all the required arguments similar to a single provider
},
label: 'Provider 1' # Differentiate the two buttons and providers in the UI
}
- {
name: 'saml_2',
args: {
name: 'saml_2', # This is mandatory and must match the provider name
strategy_class: 'OmniAuth::Strategies::SAML',
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
... # Put here all the required arguments similar to a single provider
},
label: 'Provider 2' # Differentiate the two buttons and providers in the UI
}
```
### Notes on configuring your identity provider ### Notes on configuring your identity provider
When configuring a SAML app on the IdP, you need at least: When configuring a SAML app on the IdP, you need at least:
......
...@@ -395,4 +395,170 @@ RSpec.describe AuthHelper do ...@@ -395,4 +395,170 @@ RSpec.describe AuthHelper do
end end
end end
end end
describe '#auth_strategy_class' do
subject(:auth_strategy_class) { helper.auth_strategy_class(name) }
context 'when configuration specifies no provider' do
let(:name) { 'does_not_exist' }
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
end
it 'returns false' do
expect(auth_strategy_class).to be_falsey
end
end
context 'when configuration specifies a provider with args but without strategy_class' do
let(:name) { 'google_oauth2' }
let(:provider) do
Struct.new(:name, :args).new(
name,
'app_id' => 'YOUR_APP_ID'
)
end
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
it 'returns false' do
expect(auth_strategy_class).to be_falsey
end
end
context 'when configuration specifies a provider with args and strategy_class' do
let(:name) { 'provider1' }
let(:strategy) { 'OmniAuth::Strategies::LDAP' }
let(:provider) do
Struct.new(:name, :args).new(
name,
'strategy_class' => strategy
)
end
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
it 'returns the class' do
expect(auth_strategy_class).to eq(strategy)
end
end
context 'when configuration specifies another provider with args and another strategy_class' do
let(:name) { 'provider1' }
let(:strategy) { 'OmniAuth::Strategies::LDAP' }
let(:provider) do
Struct.new(:name, :args).new(
'another_name',
'strategy_class' => strategy
)
end
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
it 'returns false' do
expect(auth_strategy_class).to be_falsey
end
end
end
describe '#saml_providers' do
subject(:saml_providers) { helper.saml_providers }
let(:saml_strategy) { 'OmniAuth::Strategies::SAML' }
let(:saml_provider_1_name) { 'saml_provider_1' }
let(:saml_provider_1) do
Struct.new(:name, :args).new(
saml_provider_1_name,
'strategy_class' => saml_strategy
)
end
let(:saml_provider_2_name) { 'saml_provider_2' }
let(:saml_provider_2) do
Struct.new(:name, :args).new(
saml_provider_2_name,
'strategy_class' => saml_strategy
)
end
let(:ldap_provider_name) { 'ldap_provider' }
let(:ldap_strategy) { 'OmniAuth::Strategies::LDAP' }
let(:ldap_provider) do
Struct.new(:name, :args).new(
ldap_provider_name,
'strategy_class' => ldap_strategy
)
end
let(:google_oauth2_provider_name) { 'google_oauth2' }
let(:google_oauth2_provider) do
Struct.new(:name, :args).new(
google_oauth2_provider_name,
'app_id' => 'YOUR_APP_ID'
)
end
context 'when configuration specifies no provider' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
end
it 'returns an empty list' do
expect(saml_providers).to be_empty
end
end
context 'when configuration specifies a provider with a SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1])
end
it 'returns the provider' do
expect(saml_providers).to match_array([saml_provider_1_name])
end
end
context 'when configuration specifies two providers with a SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2])
end
it 'returns the provider' do
expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
end
end
context 'when configuration specifies a provider with a non-SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider])
end
it 'returns an empty list' do
expect(saml_providers).to be_empty
end
end
context 'when configuration specifies four providers but only two with SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
end
it 'returns the provider' do
expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
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