Commit 1728bf98 authored by Drew Blessing's avatar Drew Blessing Committed by Drew Blessing

Minor updates to LDAP override membership API

Minor updates to `peterlloydcc` work to expose the ability to
override LDAP membership via the API. This along with `peterlloydcc`
original commit allow users to set the override flag on a member
to true or false, as needed.
parent 307c6074
...@@ -282,13 +282,14 @@ Example response: ...@@ -282,13 +282,14 @@ Example response:
} }
``` ```
### Override LDAP permissions for a member from a group ### Set override flag for a member of a group
Allows access level to be overriden for a LDAP group member > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4875) in GitLab 12.10.
>**Note:** This API endpoint is only available on 11.x Starter and above. By default, the access level of LDAP group members is set to the value specified
by LDAP through Group Sync. You can allow access level overrides by calling this endpoint.
``` ```plaintext
POST /groups/:id/members/:user_id/override POST /groups/:id/members/:user_id/override
``` ```
...@@ -317,13 +318,14 @@ Example response: ...@@ -317,13 +318,14 @@ Example response:
} }
``` ```
### Un-override LDAP permissions for a member from a group ### Remove override for a member of a group
Resets access level for a LDAP group member back to be level determined by the LDAP group > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4875) in GitLab 12.10.
>**Note:** This API endpoint is only available on 11.x Starter and above. Sets the override flag to false and allows LDAP Group Sync to reset the access
level to the LDAP-prescribed value.
``` ```plaintext
DELETE /groups/:id/members/:user_id/override DELETE /groups/:id/members/:user_id/override
``` ```
......
...@@ -20,6 +20,7 @@ module EE ...@@ -20,6 +20,7 @@ module EE
end end
scope :non_owners, -> { where("members.access_level < ?", ::Gitlab::Access::OWNER) } scope :non_owners, -> { where("members.access_level < ?", ::Gitlab::Access::OWNER) }
scope :by_user_id, ->(user_id) { where(user_id: user_id) }
end end
class_methods do class_methods do
......
--- ---
title: Add API methods to manipulate LDAP Override attribute title: Add API methods to manipulate LDAP Override attribute
merge_request: merge_request: 28674
author: Peter Lloyd <peter.lloyd@cambridgeconsultants.com> author: Peter Lloyd <peter.lloyd@cambridgeconsultants.com>
type: added type: added
...@@ -14,7 +14,7 @@ module EE ...@@ -14,7 +14,7 @@ module EE
expose :is_using_seat, if: -> (_, options) { options[:show_seat_info] } expose :is_using_seat, if: -> (_, options) { options[:show_seat_info] }
expose :override, expose :override,
if: ->(member, options) { member.source_type == 'Namespace' && member.ldap? } if: ->(member, _) { member.source_type == 'Namespace' && member.ldap? }
end end
end end
end end
......
...@@ -45,6 +45,21 @@ module EE ...@@ -45,6 +45,21 @@ module EE
member member
end end
def find_member(params)
source = find_source(:group, params.delete(:id))
authorize! :override_group_member, source
source.members.by_user_id(params[:user_id]).first
end
def present_member(updated_member)
if updated_member.valid?
present updated_member, with: ::API::Entities::Member
else
render_validation_error!(updated_member)
end
end
def log_audit_event(member) def log_audit_event(member)
::AuditEventService.new( ::AuditEventService.new(
current_user, current_user,
......
...@@ -10,55 +10,37 @@ module EE ...@@ -10,55 +10,37 @@ module EE
requires :id, type: String, desc: 'The ID of a group' requires :id, type: String, desc: 'The ID of a group'
end end
resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Overrides a member of a group.' do desc 'Overrides the access level of an LDAP group member.' do
success Entities::Member success Entities::Member
end end
params do params do
requires :user_id, type: Integer, desc: 'The user ID of the member' requires :user_id, type: Integer, desc: 'The user ID of the member'
end end
# rubocop: disable CodeReuse/ActiveRecord
post ":id/members/:user_id/override" do post ":id/members/:user_id/override" do
source = find_source(:group, params.delete(:id)) member = find_member(params)
authorize_admin_source!(:group, source)
member = source.members.find_by!(user_id: params[:user_id]) updated_member = ::Members::UpdateService
updated_member = .new(current_user, { override: true })
::Members::UpdateService .execute(member, permission: :override)
.new(current_user, { override: true })
.execute(member, permission: :override)
if updated_member.valid? present_member(updated_member)
present updated_member, with: ::API::Entities::Member
else
render_validation_error!(updated_member)
end
end end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Remove an override of a member of a group.' do desc 'Remove an LDAP group member access level override.' do
success Entities::Member success Entities::Member
end end
params do params do
requires :user_id, type: Integer, desc: 'The user ID of the member' requires :user_id, type: Integer, desc: 'The user ID of the member'
end end
# rubocop: disable CodeReuse/ActiveRecord
delete ":id/members/:user_id/override" do delete ":id/members/:user_id/override" do
source = find_source(:group, params.delete(:id)) member = find_member(params)
authorize_admin_source!(:group, source)
member = source.members.find_by!(user_id: params[:user_id]) updated_member = ::Members::UpdateService
updated_member = .new(current_user, { override: false })
::Members::UpdateService .execute(member, permission: :override)
.new(current_user, { override: false })
.execute(member, permission: :override)
if updated_member.valid? present_member(updated_member)
present updated_member, with: ::API::Entities::Member
else
render_validation_error!(updated_member)
end
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
end end
end end
......
...@@ -3,262 +3,249 @@ ...@@ -3,262 +3,249 @@
require 'spec_helper' require 'spec_helper'
describe API::Members do describe API::Members do
let(:group) { create(:group) } context 'without LDAP' do
let(:owner) { create(:user) } let(:group) { create(:group) }
let(:project) { create(:project, group: group) } let(:owner) { create(:user) }
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 describe 'POST /projects/:id/members' do
context 'group membership locked' do context 'group membership locked' do
let(:user) { 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) }
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),
params: { user_id: user.id, access_level: Member::MAINTAINER } params: { user_id: user.id, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:method_not_allowed) expect(response).to have_gitlab_http_status(:method_not_allowed)
end
end end
end end
end end
end
describe 'GET /groups/:id/members' do describe 'GET /groups/:id/members' do
it 'matches json schema' do it 'matches json schema' do
get api("/groups/#{group.to_param}/members", owner) get api("/groups/#{group.to_param}/members", owner)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/members') expect(response).to match_response_schema('public_api/v4/members')
end end
context 'when a group has SAML provider configured' do context 'when a group has SAML provider configured' do
let(:maintainer) { create(:user) } let(:maintainer) { create(:user) }
before do before do
saml_provider = create :saml_provider, group: group saml_provider = create :saml_provider, group: group
create :group_saml_identity, user: owner, saml_provider: saml_provider create :group_saml_identity, user: owner, saml_provider: saml_provider
group.add_maintainer(maintainer) group.add_maintainer(maintainer)
end end
context 'and current_user is group owner' do context 'and current_user is group owner' do
it 'returns a list of users with group SAML identities info' do it 'returns a list of users with group SAML identities info' do
get api("/groups/#{group.to_param}/members", owner) get api("/groups/#{group.to_param}/members", owner)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2) expect(json_response.size).to eq(2)
expect(json_response.first['group_saml_identity']).to match(kind_of(Hash)) expect(json_response.first['group_saml_identity']).to match(kind_of(Hash))
end end
it 'allows to filter by linked identity presence' do it 'allows to filter by linked identity presence' do
get api("/groups/#{group.to_param}/members?with_saml_identity=true", owner) get api("/groups/#{group.to_param}/members?with_saml_identity=true", owner)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(1) expect(json_response.size).to eq(1)
expect(json_response.any? { |member| member['id'] == maintainer.id }).to be_falsey expect(json_response.any? { |member| member['id'] == maintainer.id }).to be_falsey
end
end end
end
context 'and current_user is not an owner' do context 'and current_user is not an owner' do
it 'returns a list of users without group SAML identities info' do it 'returns a list of users without group SAML identities info' do
get api("/groups/#{group.to_param}/members", maintainer) get api("/groups/#{group.to_param}/members", maintainer)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response.map(&:keys).flatten).not_to include('group_saml_identity') expect(json_response.map(&:keys).flatten).not_to include('group_saml_identity')
end end
it 'ignores filter by linked identity presence' do it 'ignores filter by linked identity presence' do
get api("/groups/#{group.to_param}/members?with_saml_identity=true", maintainer) get api("/groups/#{group.to_param}/members?with_saml_identity=true", maintainer)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2) expect(json_response.size).to eq(2)
expect(json_response.any? { |member| member['id'] == maintainer.id }).to be_truthy expect(json_response.any? { |member| member['id'] == maintainer.id }).to be_truthy
end
end end
end end
end
context 'with is_using_seat' do context 'with is_using_seat' do
shared_examples 'seat information not included' do shared_examples 'seat information not included' do
it 'returns a list of users that does not contain the is_using_seat attribute' do it 'returns a list of users that does not contain the is_using_seat attribute' do
get api(api_url, owner) get api(api_url, owner)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(1) expect(json_response.size).to eq(1)
expect(json_response.first.keys).not_to include('is_using_seat') expect(json_response.first.keys).not_to include('is_using_seat')
end
end end
end
context 'with show_seat_info set to true' do context 'with show_seat_info set to true' do
it 'returns a list of users that contains the is_using_seat attribute' do it 'returns a list of users that contains the is_using_seat attribute' do
get api("/groups/#{group.to_param}/members?show_seat_info=true", owner) get api("/groups/#{group.to_param}/members?show_seat_info=true", owner)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(1) expect(json_response.size).to eq(1)
expect(json_response.first['is_using_seat']).to be_truthy expect(json_response.first['is_using_seat']).to be_truthy
end
end end
end
context 'with show_seat_info set to false' do context 'with show_seat_info set to false' do
let(:api_url) { "/groups/#{group.to_param}/members?show_seat_info=false" } let(:api_url) { "/groups/#{group.to_param}/members?show_seat_info=false" }
it_behaves_like 'seat information not included' it_behaves_like 'seat information not included'
end end
context 'with no show_seat_info set' do context 'with no show_seat_info set' do
let(:api_url) { "/groups/#{group.to_param}/members" } let(:api_url) { "/groups/#{group.to_param}/members" }
it_behaves_like 'seat information not included' it_behaves_like 'seat information not included'
end
end end
end end
end
shared_examples 'POST /:source_type/:id/members' do |source_type| shared_examples 'POST /:source_type/:id/members' do |source_type|
let(:stranger) { create(:user) } let(:stranger) { create(:user) }
let(:url) { "/#{source_type.pluralize}/#{source.id}/members" } let(:url) { "/#{source_type.pluralize}/#{source.id}/members" }
context "with :source_type == #{source_type.pluralize}" do context "with :source_type == #{source_type.pluralize}" do
it 'creates an audit event while creating a new member' do it 'creates an audit event while creating a new member' do
params = { user_id: stranger.id, access_level: Member::DEVELOPER } params = { user_id: stranger.id, access_level: Member::DEVELOPER }
expect do expect do
post api(url, owner), params: params post api(url, owner), params: params
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end.to change { AuditEvent.count }.by(1) end.to change { AuditEvent.count }.by(1)
end end
it 'does not create audit event if creating a new member fails' do it 'does not create audit event if creating a new member fails' do
params = { user_id: 0, access_level: Member::DEVELOPER } params = { user_id: 0, access_level: Member::DEVELOPER }
expect do expect do
post api(url, owner), params: params post api(url, owner), params: params
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end.not_to change { AuditEvent.count } end.not_to change { AuditEvent.count }
end
end end
end end
end
it_behaves_like 'POST /:source_type/:id/members', 'project' do it_behaves_like 'POST /:source_type/:id/members', 'project' do
let(:source) { project } let(:source) { project }
end end
it_behaves_like 'POST /:source_type/:id/members', 'group' do it_behaves_like 'POST /:source_type/:id/members', 'group' do
let(:source) { group } let(:source) { group }
end
end end
end
describe "API::Members with LDAP link" do context 'group with LDAP group link' do
let(:owner) { create(:user, username: 'owner_user') } include LdapHelpers
let(:developer) { create(:user) }
let(:ldap_developer) { create(:user) }
let(:ldap_developer2) { create(:user) }
let(:group) { create(:group_with_ldap_group_link, :public) } let(:owner) { create(:user, username: 'owner_user') }
let(:developer) { create(:user) }
let(:ldap_developer) { create(:user) }
let(:ldap_developer2) { create(:user) }
let!(:owner_member) { create(:group_member, :owner, group: group, user: owner) } let(:group) { create(:group_with_ldap_group_link, :public) }
let!(:ldap_member) { create(:group_member, :developer, group: group, user: ldap_developer, ldap: true) }
let!(:overridden_member) { create(:group_member, :developer, group: group, user: ldap_developer2, ldap: true, override: true) }
let!(:regular_member) { create(:group_member, :developer, group: group, user: developer, ldap: false) }
before do let!(:ldap_member) { create(:group_member, :developer, group: group, user: ldap_developer, ldap: true) }
# We need to actually activate the LDAP config otherwise `Group#ldap_synced?` will always be false! let!(:overridden_member) { create(:group_member, :developer, group: group, user: ldap_developer2, ldap: true, override: true) }
allow(Gitlab.config.ldap).to receive_messages(enabled: true) let!(:regular_member) { create(:group_member, :developer, group: group, user: developer, ldap: false) }
end
before do
create(:group_member, :owner, group: group, user: owner)
stub_ldap_setting(enabled: true)
end
describe 'GET /groups/:id/members/:user_id has no override attribute' do describe 'GET /groups/:id/members/:user_id' do
context 'project in a group' do it 'does not contain an override attribute for non-LDAP users in the response' do
it 'succeeds not getting override on a non-LDAP user' do
get api("/groups/#{group.id}/members/#{developer.id}", owner) get api("/groups/#{group.id}/members/#{developer.id}", owner)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(developer.id) expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER) expect(json_response['access_level']).to eq(Member::DEVELOPER)
expect(json_response['override']).to eq(nil) expect(json_response['override']).to eq(nil)
end end
end
end
describe 'GET /groups/:id/members/:user_id has an override attribute' do it 'contains an override attribute for ldap users in the response' do
context 'project in a group' do
it 'succeeds getting override on an LDAP user' do
get api("/groups/#{group.id}/members/#{ldap_developer.id}", owner) get api("/groups/#{group.id}/members/#{ldap_developer.id}", owner)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(ldap_developer.id) expect(json_response['id']).to eq(ldap_developer.id)
expect(json_response['override']).to eq(false)
expect(json_response['access_level']).to eq(Member::DEVELOPER) expect(json_response['access_level']).to eq(Member::DEVELOPER)
expect(json_response['override']).to eq(false)
end end
end end
end
describe 'POST /groups/:id/members/:user_id/override with LDAP links' do
context 'project in a group' do
it 'succeeds when override is set on an LDAP user' do
post api("/groups/#{group.id}/members/#{ldap_developer.id}/override", owner)
expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(ldap_developer.id)
expect(json_response['override']).to eq(true)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
end
describe 'PUT /groups/:id/members/:user_id' do
it 'succeeds when access_level is modified after override has been set' do it 'succeeds when access_level is modified after override has been set' do
post api("/groups/#{group.id}/members/#{ldap_developer.id}/override", owner) post api("/groups/#{group.id}/members/#{ldap_developer.id}/override", owner)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
put api("/groups/#{group.id}/members/#{ldap_developer.id}", owner), put api("/groups/#{group.id}/members/#{ldap_developer.id}", owner),
params: { access_level: Member::MAINTAINER } params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(ldap_developer.id) expect(json_response['id']).to eq(ldap_developer.id)
expect(json_response['override']).to eq(true) expect(json_response['override']).to eq(true)
expect(json_response['access_level']).to eq(Member::MAINTAINER) expect(json_response['access_level']).to eq(Member::MAINTAINER)
end end
it 'fails when access level is modified without an override' do
put api("/groups/#{group.id}/members/#{ldap_developer.id}", owner),
params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:forbidden)
end
end end
end
describe 'DELETE /groups/:id/members/:user_id/override with LDAP links' do describe 'POST /groups/:id/members/:user_id/override' do
context 'project in a group' do
it 'succeeds when override is set on an LDAP user' do it 'succeeds when override is set on an LDAP user' do
delete api("/groups/#{group.id}/members/#{ldap_developer2.id}/override", owner) post api("/groups/#{group.id}/members/#{ldap_developer.id}/override", owner)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(ldap_developer2.id) expect(json_response['id']).to eq(ldap_developer.id)
expect(json_response['override']).to eq(false) expect(json_response['override']).to eq(true)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
end end
end
end
shared_examples 'POST /:source_type/:id/members/:user_id/override' do |source_type| it 'fails when override is set for a non-ldap user' do
context "with :source_type == #{source_type.pluralize}" do post api("/groups/#{group.id}/members/#{developer.id}/override", owner)
it 'returns 403 when override is set for a non-ldap user' do
post api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}/override", owner)
expect(response).to have_gitlab_http_status(403) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
end
shared_examples 'DELETE /:source_type/:id/members/:user_id/override' do |source_type| describe 'DELETE /groups/:id/members/:user_id/override with LDAP links' do
context "with :source_type == #{source_type.pluralize}" do it 'succeeds when override is already set on an LDAP user' do
it 'returns 403 when override is set for a non-ldap user' do delete api("/groups/#{group.id}/members/#{ldap_developer2.id}/override", owner)
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}/override", owner)
expect(response).to have_gitlab_http_status(403) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(ldap_developer2.id)
expect(json_response['override']).to eq(false)
end end
end
end
it_behaves_like 'POST /:source_type/:id/members/:user_id/override', 'group' do it 'returns 403 when override is set for a non-ldap user' do
let(:source) { group } delete api("/groups/#{group.id}/members/#{developer.id}/override", owner)
end
it_behaves_like 'DELETE /:source_type/:id/members/:user_id/override', 'group' do expect(response).to have_gitlab_http_status(:forbidden)
let(:source) { group } end
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