Commit 1fa54089 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '214523-set-default-role-for-sso-in-gitlab-com' into 'master'

Resolve "Allow setting a default role for Group SSO"

See merge request gitlab-org/gitlab!37801
parents ab81882b 92a319e2
...@@ -63,10 +63,11 @@ Once you've set up your identity provider to work with GitLab, you'll need to co ...@@ -63,10 +63,11 @@ Once you've set up your identity provider to work with GitLab, you'll need to co
1. Navigate to the group's **Settings > SAML SSO**. 1. Navigate to the group's **Settings > SAML SSO**.
1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign-on URL** field. 1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign-on URL** field.
1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field. 1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field.
1. Select the access level to be applied to newly added users in the **Default membership role** field. The default access level is 'Guest'.
1. Click the **Enable SAML authentication for this group** toggle switch. 1. Click the **Enable SAML authentication for this group** toggle switch.
1. Click the **Save changes** button. 1. Click the **Save changes** button.
![Group SAML Settings for GitLab.com](img/group_saml_settings.png) ![Group SAML Settings for GitLab.com](img/group_saml_settings_v13_3.png)
NOTE: **Note:** NOTE: **Note:**
Please note that the certificate [fingerprint algorithm](#additional-providers-and-setup-options) must be in SHA1. When configuring the identity provider, use a secure signature algorithm. Please note that the certificate [fingerprint algorithm](#additional-providers-and-setup-options) must be in SHA1. When configuring the identity provider, use a secure signature algorithm.
...@@ -216,7 +217,9 @@ On subsequent visits, you should be able to go [sign in to GitLab.com with SAML] ...@@ -216,7 +217,9 @@ On subsequent visits, you should be able to go [sign in to GitLab.com with SAML]
### Role ### Role
The first time you sign in, GitLab adds you to the top-level parent group with the Guest role. Existing members with appropriate privileges can promote that new user. Starting from [GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/issues/214523), group owners can set a 'Default membership role' other than 'Guest'. To do so, [configure the SAML SSO for the group](#configuring-gitlab). That role becomes the starting access level of all users added to the group.
Existing members with appropriate privileges can promote or demote users, as needed.
If a user is already a member of the group, linking the SAML identity does not change their role. If a user is already a member of the group, linking the SAML identity does not change their role.
......
...@@ -40,7 +40,7 @@ Once [Group Single Sign-On](index.md) has been configured, we can: ...@@ -40,7 +40,7 @@ Once [Group Single Sign-On](index.md) has been configured, we can:
1. Click on the **Generate a SCIM token** button. 1. Click on the **Generate a SCIM token** button.
1. Save the token and URL so they can be used in the next step. 1. Save the token and URL so they can be used in the next step.
![SCIM token configuration](img/scim_token.png) ![SCIM token configuration](img/scim_token_v13_3.png)
## Identity Provider configuration ## Identity Provider configuration
......
...@@ -45,7 +45,7 @@ class Groups::SamlProvidersController < Groups::ApplicationController ...@@ -45,7 +45,7 @@ class Groups::SamlProvidersController < Groups::ApplicationController
end end
def saml_provider_params def saml_provider_params
allowed_params = %i[sso_url certificate_fingerprint enabled enforced_sso] allowed_params = %i[sso_url certificate_fingerprint enabled enforced_sso default_membership_role]
if Feature.enabled?(:group_managed_accounts, group) if Feature.enabled?(:group_managed_accounts, group)
allowed_params += [:enforced_group_managed_accounts, :prohibited_outer_forks] allowed_params += [:enforced_group_managed_accounts, :prohibited_outer_forks]
......
...@@ -9,6 +9,7 @@ class SamlProvider < ApplicationRecord ...@@ -9,6 +9,7 @@ class SamlProvider < ApplicationRecord
validates :group, presence: true, top_level_group: true validates :group, presence: true, top_level_group: true
validates :sso_url, presence: true, addressable_url: { schemes: %w(https), ascii_only: true } validates :sso_url, presence: true, addressable_url: { schemes: %w(https), ascii_only: true }
validates :certificate_fingerprint, presence: true, certificate_fingerprint: true validates :certificate_fingerprint, presence: true, certificate_fingerprint: true
validates :default_membership_role, presence: true, inclusion: { in: Gitlab::Access.values }
after_initialize :set_defaults, if: :new_record? after_initialize :set_defaults, if: :new_record?
......
...@@ -50,6 +50,13 @@ ...@@ -50,6 +50,13 @@
.form-text.text-muted .form-text.text-muted
= s_('GroupSAML|SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called "Thumbprint".') = s_('GroupSAML|SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called "Thumbprint".')
.well-segment.borderless.gl-mb-3.col-12.col-lg-9.gl-p-0
= f.label :default_membership_role, class: 'label-bold' do
= s_('GroupSAML|Default membership role')
= f.select :default_membership_role, options_for_select(::Gitlab::Access.options, saml_provider.default_membership_role), {}, class: 'form-control'
.form-text.text-muted
= s_('GroupSAML|This will be set as the access level of users added to the group.')
.mt-3 .mt-3
= f.submit _("Save changes"), class: 'btn btn-success', data: { qa_selector: 'save_changes_button' } = f.submit _("Save changes"), class: 'btn btn-success', data: { qa_selector: 'save_changes_button' }
#js-saml-test-button.has-tooltip.float-right #js-saml-test-button.has-tooltip.float-right
......
---
title: Allow setting a default role for Group SSO
merge_request: 37801
author:
type: added
...@@ -54,6 +54,7 @@ module API ...@@ -54,6 +54,7 @@ module API
@group = find_group(group_path) @group = find_group(group_path)
scim_not_found!(message: "Group #{group_path} not found") unless @group scim_not_found!(message: "Group #{group_path} not found") unless @group
scim_not_found!(message: "Group #{group_path} does not have SAML SSO configured") unless @group.saml_provider
check_access_to_group!(@group) check_access_to_group!(@group)
......
...@@ -8,7 +8,6 @@ module EE ...@@ -8,7 +8,6 @@ module EE
PASSWORD_AUTOMATICALLY_SET = true PASSWORD_AUTOMATICALLY_SET = true
SKIP_EMAIL_CONFIRMATION = false SKIP_EMAIL_CONFIRMATION = false
DEFAULT_ACCESS = :guest
def initialize(group, parsed_hash) def initialize(group, parsed_hash)
@group = group @group = group
...@@ -126,7 +125,7 @@ module EE ...@@ -126,7 +125,7 @@ module EE
def user_params def user_params
@parsed_hash.tap do |hash| @parsed_hash.tap do |hash|
hash[:skip_confirmation] = SKIP_EMAIL_CONFIRMATION hash[:skip_confirmation] = SKIP_EMAIL_CONFIRMATION
hash[:saml_provider_id] = @group.saml_provider&.id hash[:saml_provider_id] = @group.saml_provider.id
hash[:group_id] = @group.id hash[:group_id] = @group.id
hash[:provider] = identity_provider hash[:provider] = identity_provider
hash[:email_confirmation] = hash[:email] hash[:email_confirmation] = hash[:email]
...@@ -154,10 +153,14 @@ module EE ...@@ -154,10 +153,14 @@ module EE
strong_memoize(:member) do strong_memoize(:member) do
next @group.group_member(user) if existing_member?(user) next @group.group_member(user) if existing_member?(user)
@group.add_user(user, DEFAULT_ACCESS) if user.valid? @group.add_user(user, default_membership_role) if user.valid?
end end
end end
def default_membership_role
@group.saml_provider.default_membership_role
end
def create_identity_only? def create_identity_only?
scim_identities_enabled? && existing_user? && existing_member?(user) scim_identities_enabled? && existing_user? && existing_member?(user)
end end
......
...@@ -8,8 +8,6 @@ module EE ...@@ -8,8 +8,6 @@ module EE
delegate :user, :group, to: :identity delegate :user, :group, to: :identity
DEFAULT_ACCESS = :guest
def initialize(identity) def initialize(identity)
@identity = identity @identity = identity
end end
...@@ -24,7 +22,11 @@ module EE ...@@ -24,7 +22,11 @@ module EE
private private
def add_member def add_member
group.add_user(user, DEFAULT_ACCESS) group.add_user(user, default_membership_role)
end
def default_membership_role
group.saml_provider.default_membership_role
end end
def existing_member? def existing_member?
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
class MembershipUpdater class MembershipUpdater
attr_reader :user, :saml_provider attr_reader :user, :saml_provider
delegate :group, to: :saml_provider delegate :group, :default_membership_role, to: :saml_provider
def initialize(user, saml_provider) def initialize(user, saml_provider)
@user = user @user = user
...@@ -14,22 +14,18 @@ module Gitlab ...@@ -14,22 +14,18 @@ module Gitlab
end end
def execute def execute
return if group.member?(@user) return if group.member?(user)
member = group.add_user(@user, default_membership_level) member = group.add_user(user, default_membership_role)
log_audit_event(member: member) log_audit_event(member: member)
end end
private private
def default_membership_level
:guest
end
def log_audit_event(member:) def log_audit_event(member:)
::AuditEventService.new( ::AuditEventService.new(
@user, user,
member.source, member.source,
action: :create action: :create
).for_member(member).security_event ).for_member(member).security_event
......
...@@ -153,6 +153,19 @@ RSpec.describe Groups::OmniauthCallbacksController do ...@@ -153,6 +153,19 @@ RSpec.describe Groups::OmniauthCallbacksController do
expect(group).to be_member(user) expect(group).to be_member(user)
end end
context 'when a default access level is specified in the SAML provider' do
let!(:saml_provider) do
create(:saml_provider, group: group, default_membership_role: Gitlab::Access::DEVELOPER)
end
it 'sets the access level of the member as per the specified `default_membership_role`' do
post provider, params: { group_id: group }
created_member = group.members.find_by(user: user)
expect(created_member.access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
it_behaves_like "SAML session initiated" it_behaves_like "SAML session initiated"
it "displays a flash indicating the account has been linked" do it "displays a flash indicating the account has been linked" do
......
...@@ -116,17 +116,27 @@ RSpec.describe Groups::SamlProvidersController do ...@@ -116,17 +116,27 @@ RSpec.describe Groups::SamlProvidersController do
end end
describe 'PUT #update' do describe 'PUT #update' do
subject { put :update, params: { group_id: group, saml_provider: { enforced_sso: 'true' } } } subject do
put :update, params:
{
group_id: group,
saml_provider: {
enforced_sso: 'true',
default_membership_role: Gitlab::Access::MAINTAINER
}
}
end
before do before do
group.add_owner(user) group.add_owner(user)
end end
it 'updates the setting' do it 'updates the settings' do
expect do expect do
subject subject
saml_provider.reload saml_provider.reload
end.to change { saml_provider.enforced_sso? }.to(true) end.to change { saml_provider.enforced_sso? }.to(true)
.and change { saml_provider.default_membership_role }.to(Gitlab::Access::MAINTAINER)
end end
context 'enabling group managed when owner has linked identity' do context 'enabling group managed when owner has linked identity' do
...@@ -170,7 +180,7 @@ RSpec.describe Groups::SamlProvidersController do ...@@ -170,7 +180,7 @@ RSpec.describe Groups::SamlProvidersController do
stub_feature_flags(group_managed_accounts: true) stub_feature_flags(group_managed_accounts: true)
end end
it 'does not update update the flags' do it 'does not update the flags' do
expect do expect do
subject subject
saml_provider.reload saml_provider.reload
......
...@@ -6,6 +6,7 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do ...@@ -6,6 +6,7 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do
describe '#execute' do describe '#execute' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:service) { described_class.new(group, service_params) } let(:service) { described_class.new(group, service_params) }
let!(:saml_provider) { create(:saml_provider, group: group, default_membership_role: Gitlab::Access::DEVELOPER) }
before do before do
stub_licensed_features(group_saml: true) stub_licensed_features(group_saml: true)
...@@ -52,12 +53,18 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do ...@@ -52,12 +53,18 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do
expect(user).to be_a(User) expect(user).to be_a(User)
end end
it 'creates the member with guest access level' do context 'access level of created group member' do
service.execute let!(:saml_provider) do
create(:saml_provider, group: group, default_membership_role: Gitlab::Access::DEVELOPER)
end
access_level = group.group_member(user).access_level it 'sets the access level of the member as specified in saml_provider' do
service.execute
expect(access_level).to eq(Gitlab::Access::GUEST) access_level = group.group_member(user).access_level
expect(access_level).to eq(Gitlab::Access::DEVELOPER)
end
end end
it 'user record requires confirmation' do it 'user record requires confirmation' do
...@@ -116,7 +123,6 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do ...@@ -116,7 +123,6 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do
context 'when scim_identities is disabled' do context 'when scim_identities is disabled' do
before do before do
stub_feature_flags(scim_identities: false) stub_feature_flags(scim_identities: false)
create(:saml_provider, group: group)
end end
it_behaves_like 'scim provisioning' it_behaves_like 'scim provisioning'
...@@ -156,7 +162,6 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do ...@@ -156,7 +162,6 @@ RSpec.describe ::EE::Gitlab::Scim::ProvisioningService do
context 'when scim_identities is enabled' do context 'when scim_identities is enabled' do
before do before do
stub_feature_flags(scim_identities: true) stub_feature_flags(scim_identities: true)
create(:saml_provider, group: group)
end end
it_behaves_like 'scim provisioning' it_behaves_like 'scim provisioning'
......
...@@ -7,6 +7,9 @@ RSpec.describe ::EE::Gitlab::Scim::ReprovisionService do ...@@ -7,6 +7,9 @@ RSpec.describe ::EE::Gitlab::Scim::ReprovisionService do
let_it_be(:identity) { create(:scim_identity, active: false) } let_it_be(:identity) { create(:scim_identity, active: false) }
let_it_be(:group) { identity.group } let_it_be(:group) { identity.group }
let_it_be(:user) { identity.user } let_it_be(:user) { identity.user }
let_it_be(:saml_provider) do
create(:saml_provider, group: group, default_membership_role: Gitlab::Access::DEVELOPER)
end
let(:service) { described_class.new(identity) } let(:service) { described_class.new(identity) }
...@@ -22,12 +25,12 @@ RSpec.describe ::EE::Gitlab::Scim::ReprovisionService do ...@@ -22,12 +25,12 @@ RSpec.describe ::EE::Gitlab::Scim::ReprovisionService do
expect(group.members.pluck(:user_id)).to include(user.id) expect(group.members.pluck(:user_id)).to include(user.id)
end end
it 'creates the member with guest access level' do it 'creates the member with the access level as specified in saml_provider' do
service.execute service.execute
access_level = group.group_member(user).access_level access_level = group.group_member(user).access_level
expect(access_level).to eq(Gitlab::Access::GUEST) expect(access_level).to eq(Gitlab::Access::DEVELOPER)
end end
it 'does not change group membership when the user is already a member' do it 'does not change group membership when the user is already a member' do
......
...@@ -4,19 +4,28 @@ require 'spec_helper' ...@@ -4,19 +4,28 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth::GroupSaml::MembershipUpdater do RSpec.describe Gitlab::Auth::GroupSaml::MembershipUpdater do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:saml_provider) { create(:saml_provider) } let(:saml_provider) { create(:saml_provider, default_membership_role: Gitlab::Access::DEVELOPER) }
let(:group) { saml_provider.group } let(:group) { saml_provider.group }
it "adds the user to the group" do subject { described_class.new(user, saml_provider).execute }
described_class.new(user, saml_provider).execute
it 'adds the user to the group' do
subject
expect(group.users).to include(user) expect(group.users).to include(user)
end end
it 'adds the member with the specified `default_membership_role`' do
subject
created_member = group.members.find_by(user: user)
expect(created_member.access_level).to eq(Gitlab::Access::DEVELOPER)
end
it "doesn't duplicate group membership" do it "doesn't duplicate group membership" do
group.add_guest(user) group.add_guest(user)
described_class.new(user, saml_provider).execute subject
expect(group.members.count).to eq 1 expect(group.members.count).to eq 1
end end
...@@ -24,16 +33,16 @@ RSpec.describe Gitlab::Auth::GroupSaml::MembershipUpdater do ...@@ -24,16 +33,16 @@ RSpec.describe Gitlab::Auth::GroupSaml::MembershipUpdater do
it "doesn't overwrite existing membership level" do it "doesn't overwrite existing membership level" do
group.add_maintainer(user) group.add_maintainer(user)
described_class.new(user, saml_provider).execute subject
expect(group.members.pluck(:access_level)).to eq([Gitlab::Access::MAINTAINER]) expect(group.members.pluck(:access_level)).to eq([Gitlab::Access::MAINTAINER])
end end
it "logs an audit event" do it "logs an audit event" do
expect do expect do
described_class.new(user, saml_provider).execute subject
end.to change { AuditEvent.by_entity('Group', group).count }.by(1) end.to change { AuditEvent.by_entity('Group', group).count }.by(1)
expect(AuditEvent.last.details).to include(add: 'user_access', target_details: user.name, as: 'Guest') expect(AuditEvent.last.details).to include(add: 'user_access', target_details: user.name, as: 'Developer')
end end
end end
...@@ -20,6 +20,8 @@ RSpec.describe SamlProvider do ...@@ -20,6 +20,8 @@ RSpec.describe SamlProvider do
it { is_expected.to validate_presence_of(:group) } it { is_expected.to validate_presence_of(:group) }
it { is_expected.to validate_presence_of(:sso_url) } it { is_expected.to validate_presence_of(:sso_url) }
it { is_expected.to validate_presence_of(:certificate_fingerprint) } it { is_expected.to validate_presence_of(:certificate_fingerprint) }
it { is_expected.to validate_presence_of(:default_membership_role) }
it { is_expected.to validate_inclusion_of(:default_membership_role).in_array([10, 20, 30, 40]) }
it 'expects sso_url to be an https URL' do it 'expects sso_url to be an https URL' do
expect(subject).to allow_value('https://example.com').for(:sso_url) expect(subject).to allow_value('https://example.com').for(:sso_url)
......
This diff is collapsed.
...@@ -11930,6 +11930,9 @@ msgstr "" ...@@ -11930,6 +11930,9 @@ msgstr ""
msgid "GroupSAML|Copy SAML Response XML" msgid "GroupSAML|Copy SAML Response XML"
msgstr "" msgstr ""
msgid "GroupSAML|Default membership role"
msgstr ""
msgid "GroupSAML|Enable SAML authentication for this group." msgid "GroupSAML|Enable SAML authentication for this group."
msgstr "" msgstr ""
...@@ -12002,6 +12005,9 @@ msgstr "" ...@@ -12002,6 +12005,9 @@ msgstr ""
msgid "GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to " msgid "GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to "
msgstr "" msgstr ""
msgid "GroupSAML|This will be set as the access level of users added to the group."
msgstr ""
msgid "GroupSAML|To be able to enable enforced SSO, you first need to enable SAML authentication." msgid "GroupSAML|To be able to enable enforced SSO, you first need to enable SAML authentication."
msgstr "" msgstr ""
......
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