Commit f355870a authored by Pavel Shutsin's avatar Pavel Shutsin

Adjust group members API to include group saml info

If a group has SAML provider configured and enabled
we expose group SAML identities for group members
to group owners. It should be used at members page
to display group SAML identity for each member
parent c2bd9591
...@@ -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) }
describe 'POST /projects/:id/members' do
context 'group membership locked' do
let(:owner) { create(:user) } let(:owner) { create(:user) }
let(:group) { create(:group, membership_lock: true)}
let(:project) { create(:project, group: group) }
before do before do
group.add_owner(owner) group.add_owner(owner)
end end
describe 'POST /projects/:id/members' do
context 'group membership locked' do
let(:user) { create(:user) }
let(:group) { create(:group, membership_lock: true)}
let(:project) { create(:project, group: group) }
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