Commit 2ff0ff3a authored by Thong Kuah's avatar Thong Kuah

Merge branch '35308-list-connected-group-saml-identities' into 'master'

Adjust group members API to include group SAML info

Closes #35308

See merge request gitlab-org/gitlab!20357
parents 3741a40f f355870a
...@@ -45,6 +45,16 @@ module EE ...@@ -45,6 +45,16 @@ module EE
errors.add(:invite_email, email_no_match_email_domain(invite_email)) errors.add(:invite_email, email_no_match_email_domain(invite_email))
end end
def group_saml_identity
return unless source.saml_provider
if user.group_saml_identities.loaded?
user.group_saml_identities.detect { |i| i.saml_provider_id == source.saml_provider.id }
else
user.group_saml_identities.find_by(saml_provider: source.saml_provider)
end
end
private private
def email_no_match_email_domain(email) def email_no_match_email_domain(email)
......
...@@ -45,6 +45,10 @@ module EE ...@@ -45,6 +45,10 @@ module EE
@subject.feature_available?(:cluster_deployments) @subject.feature_available?(:cluster_deployments)
end end
condition(:group_saml_enabled) do
@subject.saml_provider&.enabled?
end
rule { reporter }.policy do rule { reporter }.policy do
enable :admin_list enable :admin_list
enable :admin_board enable :admin_board
...@@ -135,6 +139,10 @@ module EE ...@@ -135,6 +139,10 @@ module EE
rule { ip_enforcement_prevents_access & ~owner }.policy do rule { ip_enforcement_prevents_access & ~owner }.policy do
prevent :read_group prevent :read_group
end end
rule { owner & group_saml_enabled }.policy do
enable :read_group_saml_identity
end
end end
override :lookup_access_level! override :lookup_access_level!
......
---
title: Adjust group members API to include group SAML info
merge_request: 20357
author:
type: added
...@@ -79,6 +79,16 @@ module EE ...@@ -79,6 +79,16 @@ module EE
end end
end end
module Member
extend ActiveSupport::Concern
prepended do
expose :group_saml_identity,
using: ::API::Entities::Identity,
if: -> (member, options) { Ability.allowed?(options[:current_user], :read_group_saml_identity, member.source) }
end
end
module ProtectedRefAccess module ProtectedRefAccess
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module EE
module API
module Members
extend ActiveSupport::Concern
prepended do
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def retrieve_members(source, *args)
super.tap do |members|
members.includes(user: :group_saml_identities) if can_view_group_identity?(source)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def can_view_group_identity?(members_source)
can?(current_user, :read_group_saml_identity, members_source)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::Entities::Member do
subject(:entity_representation) { described_class.new(member).as_json }
let(:member) { build_stubbed(:group_member) }
let(:group_saml_identity) { build_stubbed(:group_saml_identity, extern_uid: 'TESTIDENTITY') }
before do
allow(member).to receive(:group_saml_identity).and_return(group_saml_identity)
end
context 'when current user is allowed to read group saml identity' do
before do
allow(Ability).to receive(:allowed?).with(anything, :read_group_saml_identity, member.source).and_return(true)
end
it 'exposes group_saml_identity' do
expect(entity_representation[:group_saml_identity]).to include(extern_uid: 'TESTIDENTITY')
end
end
context 'when current user is not allowed to read group saml identity' do
before do
allow(Ability).to receive(:allowed?).with(anything, :read_group_saml_identity, member.source).and_return(false)
end
it 'does not expose group saml identity' do
expect(entity_representation.keys).not_to include(:group_saml_identity)
end
end
end
...@@ -9,8 +9,8 @@ describe GroupMember do ...@@ -9,8 +9,8 @@ describe GroupMember do
describe 'validations' do describe 'validations' do
describe 'group domain limitations' do describe 'group domain limitations' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:user) { create(:user, email: 'test@gitlab.com')} let(:user) { create(:user, email: 'test@gitlab.com') }
let(:user_2) { create(:user, email: 'test@gmail.com')} let(:user_2) { create(:user, email: 'test@gmail.com') }
before do before do
create(:allowed_email_domain, group: group) create(:allowed_email_domain, group: group)
...@@ -59,4 +59,40 @@ describe GroupMember do ...@@ -59,4 +59,40 @@ describe GroupMember do
end end
end end
end end
describe '#group_saml_identity' do
subject(:group_saml_identity) { member.group_saml_identity }
let!(:member) { create :group_member }
context 'without saml_provider' do
it { is_expected.to eq nil }
end
context 'with saml_provider enabled' do
let!(:saml_provider) { create(:saml_provider, group: member.group) }
context 'when member has no connected identity' do
it { is_expected.to eq nil }
end
context 'when member has connected identity' do
let!(:group_related_identity) do
create(:group_saml_identity, user: member.user, saml_provider: saml_provider)
end
it 'returns related identity' do
expect(group_saml_identity).to eq group_related_identity
end
end
context 'when member has connected identity of different group' do
before do
create(:group_saml_identity, user: member.user)
end
it { is_expected.to eq nil }
end
end
end
end end
...@@ -445,4 +445,30 @@ describe GroupPolicy do ...@@ -445,4 +445,30 @@ describe GroupPolicy do
describe 'view_type_of_work_charts' do describe 'view_type_of_work_charts' do
include_examples 'analytics policy', :view_type_of_work_charts include_examples 'analytics policy', :view_type_of_work_charts
end end
describe '#read_group_saml_identity' do
let_it_be(:saml_provider) { create(:saml_provider, group: group, enabled: true) }
context 'for owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:read_group_saml_identity) }
context 'without Group SAML enabled' do
before do
saml_provider.update(enabled: false)
end
it { is_expected.to be_disallowed(:read_group_saml_identity) }
end
end
%w[maintainer developer reporter guest].each do |role|
context "for #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:read_group_saml_identity) }
end
end
end
end end
...@@ -3,18 +3,19 @@ ...@@ -3,18 +3,19 @@
require 'spec_helper' require 'spec_helper'
describe API::Members do describe API::Members do
let(:user) { create(:user) } let(:group) { create(:group) }
let(:owner) { create(:user) }
before do
group.add_owner(owner)
end
describe 'POST /projects/:id/members' do describe 'POST /projects/:id/members' do
context 'group membership locked' do context 'group membership locked' do
let(:owner) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group, membership_lock: true)} let(:group) { create(:group, membership_lock: true)}
let(:project) { create(:project, group: group) } let(:project) { create(:project, group: group) }
before do
group.add_owner(owner)
end
context 'project in a group' do context 'project in a group' do
it 'returns a 405 method not allowed error when group membership lock is enabled' do it 'returns a 405 method not allowed error when group membership lock is enabled' do
post api("/projects/#{project.id}/members", owner), post api("/projects/#{project.id}/members", owner),
...@@ -25,4 +26,37 @@ describe API::Members do ...@@ -25,4 +26,37 @@ describe API::Members do
end end
end end
end end
describe 'GET /groups/:id/members' do
context 'when a group has SAML provider configured' do
before do
saml_provider = create :saml_provider, group: group
create :group_saml_identity, user: owner, saml_provider: saml_provider
end
context 'and current_user is group owner' do
it 'returns a list of users with group SAML identities info' do
get api("/groups/#{group.to_param}/members", owner)
expect(response).to have_gitlab_http_status(200)
expect(json_response.first['group_saml_identity']).to match(kind_of(Hash))
end
end
context 'and current_user is not an owner' do
let(:maintainer) do
create(:user).tap do |user|
group.add_maintainer(user)
end
end
it 'returns a list of users with group SAML identities info' do
get api("/groups/#{group.to_param}/members", maintainer)
expect(response).to have_gitlab_http_status(200)
expect(json_response.map(&:keys).flatten).not_to include('group_saml_identity')
end
end
end
end
end end
...@@ -1838,6 +1838,7 @@ end ...@@ -1838,6 +1838,7 @@ end
::API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue') ::API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue')
::API::Entities::List.prepend_if_ee('EE::API::Entities::List') ::API::Entities::List.prepend_if_ee('EE::API::Entities::List')
::API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true) ::API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true)
::API::Entities::Member.prepend_if_ee('EE::API::Entities::Member', with_descendants: true)
::API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace') ::API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace')
::API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true) ::API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true)
::API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess') ::API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess')
......
...@@ -13,10 +13,19 @@ module API ...@@ -13,10 +13,19 @@ module API
authorize! :"admin_#{source_type}", source authorize! :"admin_#{source_type}", source
end end
def find_all_members(source_type, source) # rubocop: disable CodeReuse/ActiveRecord
members = source_type == 'project' ? find_all_members_for_project(source) : find_all_members_for_group(source) def retrieve_members(source, params:, deep: false)
members.non_invite members = deep ? find_all_members(source) : source.members.where.not(user_id: nil)
.non_request members = members.includes(:user)
members = members.references(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members
end
# rubocop: enable CodeReuse/ActiveRecord
def find_all_members(source)
members = source.is_a?(Project) ? find_all_members_for_project(source) : find_all_members_for_group(source)
members.non_invite.non_request
end end
def find_all_members_for_project(project) def find_all_members_for_project(project)
...@@ -26,6 +35,10 @@ module API ...@@ -26,6 +35,10 @@ module API
def find_all_members_for_group(group) def find_all_members_for_group(group)
GroupMembersFinder.new(group).execute GroupMembersFinder.new(group).execute
end end
def present_members(members)
present members, with: Entities::Member, current_user: current_user
end
end end
end end
end end
...@@ -21,18 +21,14 @@ module API ...@@ -21,18 +21,14 @@ module API
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership' optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
use :pagination use :pagination
end end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/members" do get ":id/members" do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
members = source.members.where.not(user_id: nil).includes(:user) members = paginate(retrieve_members(source, params: params))
members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members = paginate(members)
present members, with: Entities::Member present_members members
end end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Gets a list of group or project members viewable by the authenticated user, including those who gained membership through ancestor group.' do desc 'Gets a list of group or project members viewable by the authenticated user, including those who gained membership through ancestor group.' do
success Entities::Member success Entities::Member
...@@ -42,18 +38,14 @@ module API ...@@ -42,18 +38,14 @@ module API
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership' optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
use :pagination use :pagination
end end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/members/all" do get ":id/members/all" do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
members = find_all_members(source_type, source) members = paginate(retrieve_members(source, params: params, deep: true))
members = members.includes(:user).references(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members = paginate(members)
present members, with: Entities::Member present_members members
end end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Gets a member of a group or project.' do desc 'Gets a member of a group or project.' do
success Entities::Member success Entities::Member
...@@ -68,7 +60,7 @@ module API ...@@ -68,7 +60,7 @@ module API
members = source.members members = source.members
member = members.find_by!(user_id: params[:user_id]) member = members.find_by!(user_id: params[:user_id])
present member, with: Entities::Member present_members member
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -82,10 +74,10 @@ module API ...@@ -82,10 +74,10 @@ module API
get ":id/members/all/:user_id" do get ":id/members/all/:user_id" do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
members = find_all_members(source_type, source) members = find_all_members(source)
member = members.find_by!(user_id: params[:user_id]) member = members.find_by!(user_id: params[:user_id])
present member, with: Entities::Member present_members member
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -113,7 +105,7 @@ module API ...@@ -113,7 +105,7 @@ module API
if !member if !member
not_allowed! # This currently can only be reached in EE not_allowed! # This currently can only be reached in EE
elsif member.persisted? && member.valid? elsif member.persisted? && member.valid?
present member, with: Entities::Member present_members member
else else
render_validation_error!(member) render_validation_error!(member)
end end
...@@ -140,7 +132,7 @@ module API ...@@ -140,7 +132,7 @@ module API
.execute(member) .execute(member)
if updated_member.valid? if updated_member.valid?
present updated_member, with: Entities::Member present_members updated_member
else else
render_validation_error!(updated_member) render_validation_error!(updated_member)
end end
...@@ -165,3 +157,5 @@ module API ...@@ -165,3 +157,5 @@ module API
end end
end end
end end
API::Members.prepend_if_ee('EE::API::Members')
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