Commit 58d7d691 authored by Jarka Košanová's avatar Jarka Košanová Committed by Alexandru Croitor

Match Jira users by email and username/name

- add scope for matching by name OR username to User
parent b45d04a0
...@@ -509,6 +509,38 @@ class User < ApplicationRecord ...@@ -509,6 +509,38 @@ class User < ApplicationRecord
by_any_email(email, confirmed: confirmed).take by_any_email(email, confirmed: confirmed).take
end end
# Returns a relation containing all users with emails for given emails
# where non-confirmed emails are included and private commit emails are skiped
# or names where username and name is searched for exact match
#
#
# @param emails [String, Array<String>] email addresses to check
# @param names [String, Array<String>] names to check in username and name
def by_emails_or_names(jira_pairs)
sql = <<~SQL
WITH jira_users(name, email) AS (VALUES #{jira_pairs})
SELECT DISTINCT users.id AS user_id,
(CASE
WHEN ((jira_users.email = users.email) OR (jira_users.email = emails.email))
THEN 3
WHEN (jira_users.name = lower(users.username))
THEN 2
WHEN (jira_users.name = lower(users.name))
THEN 1
END)
AS match_score, jira_users.name as jira_name, jira_users.email as jira_email
FROM users
LEFT JOIN emails ON (users.id = emails.user_id)
JOIN jira_users ON (jira_users.name = lower(users.username))
OR (jira_users.name = lower(users.name))
OR (jira_users.email = users.email)
OR (jira_users.email = emails.email)
ORDER BY match_score DESC;
SQL
ActiveRecord::Base.connection.execute(sql)
end
# Returns a relation containing all the users for the given email addresses # Returns a relation containing all the users for the given email addresses
# #
# @param emails [String, Array<String>] email addresses to check # @param emails [String, Array<String>] email addresses to check
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
module JiraImport module JiraImport
class UsersImporter class UsersImporter
attr_reader :user, :project, :start_at
def initialize(user, project, start_at) def initialize(user, project, start_at)
@project = project @project = project
@start_at = start_at @start_at = start_at
...@@ -23,6 +21,8 @@ module JiraImport ...@@ -23,6 +21,8 @@ module JiraImport
private private
attr_reader :user, :project, :start_at
def mapped_users def mapped_users
users_mapper_service.execute users_mapper_service.execute
end end
...@@ -44,9 +44,9 @@ module JiraImport ...@@ -44,9 +44,9 @@ module JiraImport
# TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged # TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
case deployment_type.upcase case deployment_type.upcase
when JiraService::DEPLOYMENT_TYPES[:server] when JiraService::DEPLOYMENT_TYPES[:server]
ServerUsersMapperService.new(project.jira_service, start_at) ServerUsersMapperService.new(user, project, start_at)
when JiraService::DEPLOYMENT_TYPES[:cloud] when JiraService::DEPLOYMENT_TYPES[:cloud]
CloudUsersMapperService.new(project.jira_service, start_at) CloudUsersMapperService.new(user, project, start_at)
else else
raise ArgumentError raise ArgumentError
end end
......
...@@ -2,30 +2,37 @@ ...@@ -2,30 +2,37 @@
module JiraImport module JiraImport
class UsersMapperService class UsersMapperService
include Gitlab::Utils::StrongMemoize
# MAX_USERS must match the pageSize value in app/assets/javascripts/jira_import/utils/constants.js # MAX_USERS must match the pageSize value in app/assets/javascripts/jira_import/utils/constants.js
MAX_USERS = 50 MAX_USERS = 50
attr_reader :jira_service, :start_at # The class is called from UsersImporter and small batches of users are expected
# In case the mapping of a big batch of users is expected to be passed here
def initialize(jira_service, start_at) # the implementation needs to change here and handles the matching in batches
@jira_service = jira_service def initialize(current_user, project, start_at)
@current_user = current_user
@project = project
@jira_service = project.jira_service
@start_at = start_at @start_at = start_at
end end
def execute def execute
users.to_a.map do |jira_user| jira_users.to_a.map do |jira_user|
{ {
jira_account_id: jira_user_id(jira_user), jira_account_id: jira_user_id(jira_user),
jira_display_name: jira_user_name(jira_user), jira_display_name: jira_user_name(jira_user),
jira_email: jira_user['emailAddress'] jira_email: jira_user['emailAddress']
}.merge(match_user(jira_user)) }.merge(gitlab_id: find_gitlab_id(jira_user))
end end
end end
private private
def users attr_reader :current_user, :project, :jira_service, :start_at
@users ||= client.get(url)
def jira_users
@jira_users ||= client.get(url)
end end
def client def client
...@@ -44,10 +51,35 @@ module JiraImport ...@@ -44,10 +51,35 @@ module JiraImport
raise NotImplementedError raise NotImplementedError
end end
# TODO: Matching user by email and displayName will be done as the part def matched_users
# of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023 strong_memoize(:matched_users) do
def match_user(jira_user) pairs_to_match = jira_users.map do |user|
{ gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } "('#{jira_user_name(user)&.downcase}', '#{user['emailAddress']&.downcase}')"
end.join(',')
User.by_emails_or_names(pairs_to_match)
end
end
def find_gitlab_id(jira_user)
user = matched_users.find do |matched_user|
matched_user['jira_email'] == jira_user['emailAddress']&.downcase ||
matched_user['jira_name'].downcase == jira_user_name(jira_user)&.downcase
end
return unless user
user_id = user['user_id']
return unless project_member_ids.include?(user_id)
user_id
end
def project_member_ids
# rubocop: disable CodeReuse/ActiveRecord
@project_member_ids ||= MembersFinder.new(project, current_user).execute.pluck(:user_id)
# rubocop: enable CodeReuse/ActiveRecord
end end
end end
end end
...@@ -5,15 +5,44 @@ require 'spec_helper' ...@@ -5,15 +5,44 @@ require 'spec_helper'
RSpec.describe JiraImport::CloudUsersMapperService do RSpec.describe JiraImport::CloudUsersMapperService do
let(:start_at) { 7 } let(:start_at) { 7 }
let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" } let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
let_it_be(:user_1) { create(:user, username: 'randomuser', name: 'USER-name1', email: 'uji@example.com') }
let_it_be(:user_2) { create(:user, username: 'username2') }
let_it_be(:user_5) { create(:user, username: 'username5') }
let_it_be(:user_4) { create(:user, email: 'user4@example.com') }
let_it_be(:user_6) { create(:user, email: 'user6@example.com') }
let_it_be(:user_7) { create(:user, username: 'username7') }
let_it_be(:user_8) do
create(:user).tap { |user| create(:email, user: user, email: 'user8_email@example.com') }
end
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let(:jira_users) do let(:jira_users) do
[ [
{ 'accountId' => 'abcd', 'displayName' => 'user1' }, { 'accountId' => 'abcd', 'displayName' => 'User-Name1' }, # matched by name
{ 'accountId' => 'efg' }, { 'accountId' => 'efg', 'displayName' => 'username2' }, # matcher by username
{ 'accountId' => 'hij', 'displayName' => 'user3', 'emailAddress' => 'user3@example.com' } { 'accountId' => 'hij' }, # no match
{ 'accountId' => '123', 'displayName' => 'user4', 'emailAddress' => 'user4@example.com' }, # matched by email
{ 'accountId' => '456', 'displayName' => 'username5foo', 'emailAddress' => 'user5@example.com' }, # no match
{ 'accountId' => '789', 'displayName' => 'user6', 'emailAddress' => 'user6@example.com' }, # matched by email, no project member
{ 'accountId' => 'xyz', 'displayName' => 'username7', 'emailAddress' => 'user7@example.com' }, # matched by username, no project member
{ 'accountId' => 'vhk', 'displayName' => 'user8', 'emailAddress' => 'user8_email@example.com' }, # matched by secondary email
{ 'accountId' => 'uji', 'displayName' => 'user9', 'emailAddress' => 'uji@example.com' } # matched by email, same as user_1
] ]
end end
describe '#execute' do describe '#execute' do
before do
project.add_developer(current_user)
project.add_developer(user_1)
project.add_developer(user_2)
group.add_developer(user_4)
group.add_guest(user_8)
end
it_behaves_like 'mapping jira users' it_behaves_like 'mapping jira users'
end end
end end
...@@ -5,15 +5,44 @@ require 'spec_helper' ...@@ -5,15 +5,44 @@ require 'spec_helper'
RSpec.describe JiraImport::ServerUsersMapperService do RSpec.describe JiraImport::ServerUsersMapperService do
let(:start_at) { 7 } let(:start_at) { 7 }
let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" } let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
let_it_be(:user_1) { create(:user, username: 'randomuser', name: 'USER-name1', email: 'uji@example.com') }
let_it_be(:user_2) { create(:user, username: 'username2') }
let_it_be(:user_5) { create(:user, username: 'username5') }
let_it_be(:user_4) { create(:user, email: 'user4@example.com') }
let_it_be(:user_6) { create(:user, email: 'user6@example.com') }
let_it_be(:user_7) { create(:user, username: 'username7') }
let_it_be(:user_8) do
create(:user).tap { |user| create(:email, user: user, email: 'user8_email@example.com') }
end
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let(:jira_users) do let(:jira_users) do
[ [
{ 'key' => 'abcd', 'name' => 'user1' }, { 'key' => 'abcd', 'name' => 'User-Name1' }, # matched by name
{ 'key' => 'efg' }, { 'key' => 'efg', 'name' => 'username2' }, # matcher by username
{ 'key' => 'hij', 'name' => 'user3', 'emailAddress' => 'user3@example.com' } { 'key' => 'hij' }, # no match
{ 'key' => '123', 'name' => 'user4', 'emailAddress' => 'user4@example.com' }, # matched by email
{ 'key' => '456', 'name' => 'username5foo', 'emailAddress' => 'user5@example.com' }, # no match
{ 'key' => '789', 'name' => 'user6', 'emailAddress' => 'user6@example.com' }, # matched by email, no project member
{ 'key' => 'xyz', 'name' => 'username7', 'emailAddress' => 'user7@example.com' }, # matched by username, no project member
{ 'key' => 'vhk', 'name' => 'user8', 'emailAddress' => 'user8_email@example.com' }, # matched by secondary email
{ 'key' => 'uji', 'name' => 'user9', 'emailAddress' => 'uji@example.com' } # matched by email, same as user_1
] ]
end end
describe '#execute' do describe '#execute' do
before do
project.add_developer(current_user)
project.add_developer(user_1)
project.add_developer(user_2)
group.add_developer(user_4)
group.add_guest(user_8)
end
it_behaves_like 'mapping jira users' it_behaves_like 'mapping jira users'
end end
end end
...@@ -6,7 +6,8 @@ RSpec.describe JiraImport::UsersImporter do ...@@ -6,7 +6,8 @@ RSpec.describe JiraImport::UsersImporter do
include JiraServiceHelper include JiraServiceHelper
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project) } let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, group: group) }
let_it_be(:start_at) { 7 } let_it_be(:start_at) { 7 }
let(:importer) { described_class.new(user, project, start_at) } let(:importer) { described_class.new(user, project, start_at) }
...@@ -18,19 +19,15 @@ RSpec.describe JiraImport::UsersImporter do ...@@ -18,19 +19,15 @@ RSpec.describe JiraImport::UsersImporter do
[ [
{ {
jira_account_id: 'acc1', jira_account_id: 'acc1',
jira_display_name: 'user1', jira_display_name: 'user-name1',
jira_email: 'sample@jira.com', jira_email: 'sample@jira.com',
gitlab_id: nil, gitlab_id: project_member.id
gitlab_username: nil,
gitlab_name: nil
}, },
{ {
jira_account_id: 'acc2', jira_account_id: 'acc2',
jira_display_name: 'user2', jira_display_name: 'user-name2',
jira_email: nil, jira_email: nil,
gitlab_id: nil, gitlab_id: group_member.id
gitlab_username: nil,
gitlab_name: nil
} }
] ]
end end
...@@ -69,13 +66,22 @@ RSpec.describe JiraImport::UsersImporter do ...@@ -69,13 +66,22 @@ RSpec.describe JiraImport::UsersImporter do
context 'when jira client returns an empty array' do context 'when jira client returns an empty array' do
let(:jira_users) { [] } let(:jira_users) { [] }
it 'retturns nil payload' do it 'returns nil payload' do
expect(subject.success?).to be_truthy expect(subject.success?).to be_truthy
expect(subject.payload).to be_empty expect(subject.payload).to be_empty
end end
end end
context 'when jira client returns an results' do context 'when jira client returns an results' do
let_it_be(:project_member) { create(:user, email: 'sample@jira.com') }
let_it_be(:group_member) { create(:user, name: 'user-name2') }
let_it_be(:other_user) { create(:user) }
before do
project.add_developer(project_member)
group.add_developer(group_member)
end
it 'returns the mapped users' do it 'returns the mapped users' do
expect(subject.success?).to be_truthy expect(subject.success?).to be_truthy
expect(subject.payload).to eq(mapped_users) expect(subject.payload).to eq(mapped_users)
...@@ -90,8 +96,8 @@ RSpec.describe JiraImport::UsersImporter do ...@@ -90,8 +96,8 @@ RSpec.describe JiraImport::UsersImporter do
let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" } let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
let(:jira_users) do let(:jira_users) do
[ [
{ 'key' => 'acc1', 'name' => 'user1', 'emailAddress' => 'sample@jira.com' }, { 'key' => 'acc1', 'name' => 'user-name1', 'emailAddress' => 'sample@jira.com' },
{ 'key' => 'acc2', 'name' => 'user2' } { 'key' => 'acc2', 'name' => 'user-name2' }
] ]
end end
...@@ -110,8 +116,8 @@ RSpec.describe JiraImport::UsersImporter do ...@@ -110,8 +116,8 @@ RSpec.describe JiraImport::UsersImporter do
let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" } let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
let(:jira_users) do let(:jira_users) do
[ [
{ 'accountId' => 'acc1', 'displayName' => 'user1', 'emailAddress' => 'sample@jira.com' }, { 'accountId' => 'acc1', 'displayName' => 'user-name1', 'emailAddress' => 'sample@jira.com' },
{ 'accountId' => 'acc2', 'displayName' => 'user2' } { 'accountId' => 'acc2', 'displayName' => 'user-name2' }
] ]
end end
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
RSpec.shared_examples 'mapping jira users' do RSpec.shared_examples 'mapping jira users' do
let(:client) { double } let(:client) { double }
let_it_be(:project) { create(:project) }
let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } let_it_be(:jira_service) { create(:jira_service, project: project, active: true) }
before do before do
...@@ -11,7 +10,7 @@ RSpec.shared_examples 'mapping jira users' do ...@@ -11,7 +10,7 @@ RSpec.shared_examples 'mapping jira users' do
allow(client).to receive(:get).with(url).and_return(jira_users) allow(client).to receive(:get).with(url).and_return(jira_users)
end end
subject { described_class.new(jira_service, start_at) } subject { described_class.new(current_user, project, start_at) }
context 'jira_users is nil' do context 'jira_users is nil' do
let(:jira_users) { nil } let(:jira_users) { nil }
...@@ -22,18 +21,27 @@ RSpec.shared_examples 'mapping jira users' do ...@@ -22,18 +21,27 @@ RSpec.shared_examples 'mapping jira users' do
end end
context 'when jira_users is present' do context 'when jira_users is present' do
# TODO: now we only create an array in a proper format
# mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023
let(:mapped_users) do let(:mapped_users) do
[ [
{ jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, { jira_account_id: 'abcd', jira_display_name: 'User-Name1', jira_email: nil, gitlab_id: user_1.id },
{ jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, { jira_account_id: 'efg', jira_display_name: 'username2', jira_email: nil, gitlab_id: user_2.id },
{ jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } { jira_account_id: 'hij', jira_display_name: nil, jira_email: nil, gitlab_id: nil },
{ jira_account_id: '123', jira_display_name: 'user4', jira_email: 'user4@example.com', gitlab_id: user_4.id },
{ jira_account_id: '456', jira_display_name: 'username5foo', jira_email: 'user5@example.com', gitlab_id: nil },
{ jira_account_id: '789', jira_display_name: 'user6', jira_email: 'user6@example.com', gitlab_id: nil },
{ jira_account_id: 'xyz', jira_display_name: 'username7', jira_email: 'user7@example.com', gitlab_id: nil },
{ jira_account_id: 'vhk', jira_display_name: 'user8', jira_email: 'user8_email@example.com', gitlab_id: user_8.id },
{ jira_account_id: 'uji', jira_display_name: 'user9', jira_email: 'uji@example.com', gitlab_id: user_1.id }
] ]
end end
it 'returns users mapped to Gitlab' do it 'returns users mapped to Gitlab' do
expect(subject.execute).to eq(mapped_users) expect(subject.execute).to eq(mapped_users)
end end
# 1 query for getting matched users, 3 queries for MembersFinder
it 'runs only 4 queries' do
expect { subject }.not_to exceed_query_limit(4)
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