Commit fd9ceec5 authored by James Lopez's avatar James Lopez

Merge branch '11540-support-emails-as-id-in-scim' into 'master'

Resolve "Support emails as ID in SCIM"

Closes #11540

See merge request gitlab-org/gitlab-ee!14625
parents 43cfc786 a81db23b
---
title: Support emails as ID in SCIM
merge_request: 14625
author:
type: fixed
...@@ -5,6 +5,7 @@ module API ...@@ -5,6 +5,7 @@ module API
prefix 'api/scim' prefix 'api/scim'
version 'v2' version 'v2'
content_type :json, 'application/scim+json' content_type :json, 'application/scim+json'
USER_ID_REQUIREMENTS = { id: /.+/ }.freeze
namespace 'groups/:group' do namespace 'groups/:group' do
params do params do
...@@ -107,7 +108,7 @@ module API ...@@ -107,7 +108,7 @@ module API
desc 'Get a SAML user' do desc 'Get a SAML user' do
detail 'This feature was introduced in GitLab 11.10.' detail 'This feature was introduced in GitLab 11.10.'
end end
get ':id' do get ':id', requirements: USER_ID_REQUIREMENTS do
group = find_and_authenticate_group!(params[:group]) group = find_and_authenticate_group!(params[:group])
identity = GroupSamlIdentityFinder.find_by_group_and_uid(group: group, uid: params[:id]) identity = GroupSamlIdentityFinder.find_by_group_and_uid(group: group, uid: params[:id])
...@@ -142,7 +143,7 @@ module API ...@@ -142,7 +143,7 @@ module API
desc 'Updates a SAML user' do desc 'Updates a SAML user' do
detail 'This feature was introduced in GitLab 11.10.' detail 'This feature was introduced in GitLab 11.10.'
end end
patch ':id' do patch ':id', requirements: USER_ID_REQUIREMENTS do
scim_error!(message: 'Missing ID') unless params[:id] scim_error!(message: 'Missing ID') unless params[:id]
group = find_and_authenticate_group!(params[:group]) group = find_and_authenticate_group!(params[:group])
...@@ -164,7 +165,7 @@ module API ...@@ -164,7 +165,7 @@ module API
desc 'Removes a SAML user' do desc 'Removes a SAML user' do
detail 'This feature was introduced in GitLab 11.10.' detail 'This feature was introduced in GitLab 11.10.'
end end
delete ":id" do delete ':id', requirements: USER_ID_REQUIREMENTS do
scim_error!(message: 'Missing ID') unless params[:id] scim_error!(message: 'Missing ID') unless params[:id]
group = find_and_authenticate_group!(params[:group]) group = find_and_authenticate_group!(params[:group])
......
...@@ -4,7 +4,6 @@ require 'spec_helper' ...@@ -4,7 +4,6 @@ require 'spec_helper'
describe API::Scim do describe API::Scim do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:identity) { create(:group_saml_identity, user: user) }
let(:group) { identity.saml_provider.group } let(:group) { identity.saml_provider.group }
let(:scim_token) { create(:scim_oauth_access_token, group: group) } let(:scim_token) { create(:scim_oauth_access_token, group: group) }
...@@ -14,216 +13,234 @@ describe API::Scim do ...@@ -14,216 +13,234 @@ describe API::Scim do
group.add_owner(user) group.add_owner(user)
end end
describe 'GET api/scim/v2/groups/:group/Users' do shared_examples 'SCIM API Endpoints' do
context 'without token auth' do describe 'GET api/scim/v2/groups/:group/Users' do
it 'responds with 401' do context 'without token auth' do
get scim_api("scim/v2/groups/#{group.full_path}/Users?filter=id eq \"#{identity.extern_uid}\"", token: false) it 'responds with 401' do
get scim_api("scim/v2/groups/#{group.full_path}/Users?filter=id eq \"#{identity.extern_uid}\"", token: false)
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end
end end
end
it 'responds with an error if there is no filter' do it 'responds with an error if there is no filter' do
get scim_api("scim/v2/groups/#{group.full_path}/Users") get scim_api("scim/v2/groups/#{group.full_path}/Users")
expect(response).to have_gitlab_http_status(412)
end
context 'existing user' do expect(response).to have_gitlab_http_status(412)
it 'responds with 200' do
get scim_api("scim/v2/groups/#{group.full_path}/Users?filter=id eq \"#{identity.extern_uid}\"")
expect(response).to have_gitlab_http_status(200)
expect(json_response['Resources']).not_to be_empty
expect(json_response['totalResults']).to eq(1)
end end
end
context 'no user' do context 'existing user' do
it 'responds with 200' do it 'responds with 200' do
get scim_api("scim/v2/groups/#{group.full_path}/Users?filter=id eq \"nonexistent\"") get scim_api("scim/v2/groups/#{group.full_path}/Users?filter=id eq \"#{identity.extern_uid}\"")
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['Resources']).to be_empty expect(json_response['Resources']).not_to be_empty
expect(json_response['totalResults']).to eq(0) expect(json_response['totalResults']).to eq(1)
end
end end
end
end
describe 'GET api/scim/v2/groups/:group/Users/:id' do
it 'responds with 404 if there is no user' do
get scim_api("scim/v2/groups/#{group.full_path}/Users/123")
expect(response).to have_gitlab_http_status(404) context 'no user' do
end it 'responds with 200' do
get scim_api("scim/v2/groups/#{group.full_path}/Users?filter=id eq \"nonexistent\"")
context 'existing user' do
it 'responds with 200' do
get scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}")
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(identity.extern_uid) expect(json_response['Resources']).to be_empty
expect(json_response['totalResults']).to eq(0)
end
end end
end end
end
describe 'POST api/scim/v2/groups/:group/Users' do
let(:post_params) do
{
externalId: 'test_uid',
active: nil,
userName: 'username',
emails: [
{ primary: true, type: 'work', value: 'work@example.com' }
],
name: { formatted: 'Test Name', familyName: 'Name', givenName: 'Test' }
}.to_query
end
context 'without an existing user' do
let(:new_user) { User.find_by_email('work@example.com') }
let(:member) { GroupMember.find_by(user: new_user, group: group) }
before do
post scim_api("scim/v2/groups/#{group.full_path}/Users?params=#{post_params}")
end
it 'responds with 201' do describe 'GET api/scim/v2/groups/:group/Users/:id' do
expect(response).to have_gitlab_http_status(201) it 'responds with 404 if there is no user' do
end get scim_api("scim/v2/groups/#{group.full_path}/Users/123")
it 'has the user external ID' do
expect(json_response['id']).to eq('test_uid')
end
it 'has the email' do expect(response).to have_gitlab_http_status(404)
expect(json_response['emails'].first['value']).to eq('work@example.com')
end end
it 'created the identity' do context 'existing user' do
expect(Identity.find_by_extern_uid(:group_saml, 'test_uid')).not_to be_nil it 'responds with 200' do
end get scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}")
it 'has the right saml provider' do
identity = Identity.find_by_extern_uid(:group_saml, 'test_uid')
expect(identity.saml_provider_id).to eq(group.saml_provider.id) expect(response).to have_gitlab_http_status(200)
end expect(json_response['id']).to eq(identity.extern_uid)
end
it 'created the user' do
expect(new_user).not_to be_nil
end
it 'created the right member' do
expect(member.access_level).to eq(::Gitlab::Access::GUEST)
end end
end end
context 'existing user' do describe 'POST api/scim/v2/groups/:group/Users' do
before do let(:post_params) do
old_user = create(:user, email: 'work@example.com') {
externalId: 'test_uid',
active: nil,
userName: 'username',
emails: [
{ primary: true, type: 'work', value: 'work@example.com' }
],
name: { formatted: 'Test Name', familyName: 'Name', givenName: 'Test' }
}.to_query
end
create(:group_saml_identity, user: old_user, extern_uid: 'test_uid') context 'without an existing user' do
group.add_guest(old_user) let(:new_user) { User.find_by_email('work@example.com') }
let(:member) { GroupMember.find_by(user: new_user, group: group) }
post scim_api("scim/v2/groups/#{group.full_path}/Users?params=#{post_params}") before do
end post scim_api("scim/v2/groups/#{group.full_path}/Users?params=#{post_params}")
end
it 'responds with 201' do it 'responds with 201' do
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
end end
it 'has the user external ID' do it 'has the user external ID' do
expect(json_response['id']).to eq('test_uid') expect(json_response['id']).to eq('test_uid')
end end
end
end
describe 'PATCH api/scim/v2/groups/:group/Users/:id' do it 'has the email' do
it 'responds with 404 if there is no user' do expect(json_response['emails'].first['value']).to eq('work@example.com')
patch scim_api("scim/v2/groups/#{group.full_path}/Users/123") end
expect(response).to have_gitlab_http_status(404) it 'created the identity' do
end expect(Identity.find_by_extern_uid(:group_saml, 'test_uid')).not_to be_nil
end
context 'existing user' do it 'has the right saml provider' do
context 'extern UID' do identity = Identity.find_by_extern_uid(:group_saml, 'test_uid')
before do
params = { Operations: [{ 'op': 'Replace', 'path': 'id', 'value': 'new_uid' }] }.to_query
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}") expect(identity.saml_provider_id).to eq(group.saml_provider.id)
end end
it 'responds with 204' do it 'created the user' do
expect(response).to have_gitlab_http_status(204) expect(new_user).not_to be_nil
end end
it 'updates the extern_uid' do it 'created the right member' do
expect(identity.reload.extern_uid).to eq('new_uid') expect(member.access_level).to eq(::Gitlab::Access::GUEST)
end end
end end
context 'name' do context 'existing user' do
before do before do
params = { Operations: [{ 'op': 'Replace', 'path': 'name.formatted', 'value': 'new_name' }] }.to_query old_user = create(:user, email: 'work@example.com')
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}") create(:group_saml_identity, user: old_user, extern_uid: 'test_uid')
end group.add_guest(old_user)
it 'responds with 204' do post scim_api("scim/v2/groups/#{group.full_path}/Users?params=#{post_params}")
expect(response).to have_gitlab_http_status(204)
end end
it 'updates the name' do it 'responds with 201' do
expect(user.reload.name).to eq('new_name') expect(response).to have_gitlab_http_status(201)
end end
it 'responds with an empty response' do it 'has the user external ID' do
expect(json_response).to eq({}) expect(json_response['id']).to eq('test_uid')
end end
end end
end
describe 'PATCH api/scim/v2/groups/:group/Users/:id' do
it 'responds with 404 if there is no user' do
patch scim_api("scim/v2/groups/#{group.full_path}/Users/123")
expect(response).to have_gitlab_http_status(404)
end
context 'email' do context 'existing user' do
context 'non existent email' do context 'extern UID' do
before do before do
params = { Operations: [{ 'op': 'Replace', 'path': 'emails[type eq "work"].value', 'value': 'new@mail.com' }] }.to_query params = { Operations: [{ 'op': 'Replace', 'path': 'id', 'value': 'new_uid' }] }.to_query
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}") patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}")
end end
it 'updates the email' do it 'responds with 204' do
expect(user.reload.unconfirmed_email).to eq('new@mail.com') expect(response).to have_gitlab_http_status(204)
end
it 'updates the extern_uid' do
expect(identity.reload.extern_uid).to eq('new_uid')
end
end
context 'name' do
before do
params = { Operations: [{ 'op': 'Replace', 'path': 'name.formatted', 'value': 'new_name' }] }.to_query
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}")
end end
it 'responds with 204' do it 'responds with 204' do
expect(response).to have_gitlab_http_status(204) expect(response).to have_gitlab_http_status(204)
end end
it 'updates the name' do
expect(user.reload.name).to eq('new_name')
end
it 'responds with an empty response' do
expect(json_response).to eq({})
end
end end
context 'existent email' do context 'email' do
before do context 'non existent email' do
create(:user, email: 'new@mail.com') before do
params = { Operations: [{ 'op': 'Replace', 'path': 'emails[type eq "work"].value', 'value': 'new@mail.com' }] }.to_query
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}")
end
it 'updates the email' do
expect(user.reload.unconfirmed_email).to eq('new@mail.com')
end
it 'responds with 204' do
expect(response).to have_gitlab_http_status(204)
end
end
params = { Operations: [{ 'op': 'Replace', 'path': 'emails[type eq "work"].value', 'value': 'new@mail.com' }] }.to_query context 'existent email' do
before do
create(:user, email: 'new@mail.com')
params = { Operations: [{ 'op': 'Replace', 'path': 'emails[type eq "work"].value', 'value': 'new@mail.com' }] }.to_query
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}")
end
it 'does not update a duplicated email' do
expect(user.reload.unconfirmed_email).not_to eq('new@mail.com')
end
it 'responds with 209' do
expect(response).to have_gitlab_http_status(409)
end
end
end
context 'Remove user' do
before do
params = { Operations: [{ 'op': 'Replace', 'path': 'active', 'value': 'False' }] }.to_query
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}") patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}")
end end
it 'does not update a duplicated email' do it 'responds with 204' do
expect(user.reload.unconfirmed_email).not_to eq('new@mail.com') expect(response).to have_gitlab_http_status(204)
end end
it 'responds with 209' do it 'removes the identity link' do
expect(response).to have_gitlab_http_status(409) expect { identity.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end
end
context 'Remove user' do describe 'DELETE /scim/v2/groups/:group/Users/:id' do
context 'existing user' do
before do before do
params = { Operations: [{ 'op': 'Replace', 'path': 'active', 'value': 'False' }] }.to_query delete scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}")
patch scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}?#{params}")
end end
it 'responds with 204' do it 'responds with 204' do
...@@ -233,37 +250,33 @@ describe API::Scim do ...@@ -233,37 +250,33 @@ describe API::Scim do
it 'removes the identity link' do it 'removes the identity link' do
expect { identity.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { identity.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
end
end
end
describe 'DELETE /scim/v2/groups/:group/Users/:id' do it 'responds with an empty response' do
context 'existing user' do expect(json_response).to eq({})
before do end
delete scim_api("scim/v2/groups/#{group.full_path}/Users/#{identity.extern_uid}")
end end
it 'responds with 204' do it 'responds with 404 if there is no user' do
expect(response).to have_gitlab_http_status(204) delete scim_api("scim/v2/groups/#{group.full_path}/Users/123")
end
it 'removes the identity link' do expect(response).to have_gitlab_http_status(404)
expect { identity.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
end
it 'responds with an empty response' do def scim_api(url, token: true)
expect(json_response).to eq({}) api(url, user, version: '', oauth_access_token: token ? scim_token : nil)
end
end end
end
it 'responds with 404 if there is no user' do context 'user with an alphanumeric extern_uid' do
delete scim_api("scim/v2/groups/#{group.full_path}/Users/123") let(:identity) { create(:group_saml_identity, user: user, extern_uid: generate(:username)) }
expect(response).to have_gitlab_http_status(404) it_behaves_like 'SCIM API Endpoints'
end
end end
def scim_api(url, token: true) context 'user with an email extern_uid' do
api(url, user, version: '', oauth_access_token: token ? scim_token : nil) let(:identity) { create(:group_saml_identity, user: user, extern_uid: user.email) }
it_behaves_like 'SCIM API Endpoints'
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