MSFT Sync: support no-address upns

closes INTEROP-6637
flag=microsoft_group_enrollments_syncing

test-plan:
* Have a course with student and teacher enrolled;
* Have a Microsoft group recorded to this course;
* Keep sure these users have a CommunicationChannel and Pseudonym;
* To check the `email` login attribute I used the demo accounts:
  demostudent and demoteacher;
* To check the `preferred_username` login attribute I used the interop
  accounts: interop_student and interop_teacher;
* Helpers:
  ID = 5
  TENANT='sample'
  def redo_g
    MicrosoftSync::UserMapping.destroy_all
    course = Course.find(ID)
    group = MicrosoftSync::Group.find(ID)
    gs = MicrosoftSync::GraphService.new(TENANT)
    gs.request(:delete, "groups/#{group.ms_group_id}")
    group.destroy!
    group = MicrosoftSync::Group.find(ID)
    group.restore!
    group
  end

  def run_sync
    group = MicrosoftSync::Group.find(ID)
    group.syncer_job.run_synchronously
  end

  def run_later
    group = MicrosoftSync::Group.find(ID)
    group.syncer_job.run_later
  end

  def set_login_attribute_to(new_value)
    group = MicrosoftSync::Group.find(ID)
    root_account = group.root_account
    root_account.settings[:microsoft_sync_login_attribute] = new_value
    root_account.save
  end
* Into rails console you should be able to change the login_attribute
  and sync the team:
  $ redo_g
  $ set_login_attribute_to('email')
  $ run_sync # or run_later
* You should be able to run the sync with `email` and
  `preferred_username`;
* You should check in the microsoft admin that the team was created with
  the correct `email` or `preferred_username` configured;
* I didn't test with `sis_user_id` login attribute and
  `preferred_username` login attribute when it's a GUID;

Change-Id: I38eaf862bbcfb2e489a2c83f13cc985a4209b108
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/263130
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Product-Review: Wagner Goncalves <wagner.goncalves@instructure.com>
Reviewed-by: Evan Battaglia <ebattaglia@instructure.com>
QA-Review: Evan Battaglia <ebattaglia@instructure.com>
This commit is contained in:
Wagner Gonçalves 2021-04-16 18:31:32 -03:00 committed by Wagner Goncalves
parent 8e88be86eb
commit 60c1820b8f
4 changed files with 235 additions and 2 deletions

View File

@ -126,8 +126,8 @@ module MicrosoftSync
MicrosoftSync::UserMapping.find_enrolled_user_ids_without_mappings(
course: course, batch_size: ENROLLMENTS_UPN_FETCHING_BATCH_SIZE
) do |user_ids|
users_and_upns = CommunicationChannel.
where(user_id: user_ids, path_type: 'email').pluck(:user_id, :path)
users_upns_finder = MicrosoftSync::UsersUpnsFinder.new(user_ids, group.root_account)
users_and_upns = users_upns_finder.call
users_and_upns.each_slice(GraphServiceHelpers::USERS_UPNS_TO_AADS_BATCH_SIZE) do |slice|
upn_to_aad = graph_service_helpers.users_upns_to_aads(slice.map(&:last))

View File

@ -0,0 +1,104 @@
# frozen_string_literal: true
#
# Copyright (C) 2021 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# Is responsible for finding the the user's UPNs according to the
# microsoft_sync_login_attribute in the Account settings
#
module MicrosoftSync
# When `login_attribute` is not set or is one that we don't know how find the
# Canvas user id information, we'll raise and exception and stop the job
class InvalidOrMissingLoginAttributeConfig < StandardError
include StateMachineJob::GracefulCancelErrorMixin
end
class UsersUpnsFinder
attr_reader :user_ids, :root_account
delegate :settings, to: :root_account
def initialize(user_ids, root_account)
@user_ids = user_ids
@root_account = root_account
end
def call
return [] if user_ids.blank? || root_account.blank?
case login_attribute
when 'email' then find_by_email
when 'preferred_username' then find_by_preferred_username
when 'sis_user_id' then find_by_sis_user_id
else raise InvalidOrMissingLoginAttributeConfig
end
end
private
def find_by_email
users_upns = CommunicationChannel
.where(user_id: user_ids, path_type: 'email')
.order(position: :asc)
.pluck(:user_id, :path)
uniq_upn_by_user_id(users_upns)
end
def find_by_preferred_username
users_upns = find_active_pseudonyms.pluck(:user_id, :unique_id)
uniq_upn_by_user_id(users_upns)
end
def find_by_sis_user_id
users_upns = find_active_pseudonyms.pluck(:user_id, :sis_user_id)
uniq_upn_by_user_id(users_upns)
end
def find_active_pseudonyms
root_account.pseudonyms.active.where(user_id: user_ids).order(position: :asc)
end
def login_attribute
@login_attribute ||= begin
enabled = settings[:microsoft_sync_enabled]
login_attribute = settings[:microsoft_sync_login_attribute]
raise InvalidOrMissingLoginAttributeConfig unless enabled && login_attribute
login_attribute
end
end
# The user can have more than one communication channel/pseudonym, so we're
# ordering the users_upns by position ASC (the highest position is the
# smallest number) and returning the first upn found to the related user_id.
def uniq_upn_by_user_id(users_upns)
return [] unless users_upns
response = {}
users_upns.each { |user_id, upn| response[user_id] ||= upn }
response.to_a
end
end
end

View File

@ -72,6 +72,7 @@ describe MicrosoftSync::SyncerSteps do
ra = course.root_account
ra.settings[:microsoft_sync_enabled] = sync_enabled
ra.settings[:microsoft_sync_tenant] = tenant
ra.settings[:microsoft_sync_login_attribute] = 'email'
ra.save!
allow(MicrosoftSync::GraphServiceHelpers).to \

View File

@ -0,0 +1,128 @@
# frozen_string_literal: true
#
# Copyright (C) 2021 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require 'spec_helper'
describe MicrosoftSync::UsersUpnsFinder do
let(:course) { course_model(name: 'sync test course') }
let(:group) { MicrosoftSync::Group.create(course: course) }
let(:root_account) { group.root_account }
describe '#call' do
subject { described_class.new(user_ids, root_account).call }
context 'when microsoft sync is not configured' do
let(:user_ids) { [1] }
it 'raise an error' do
expect { subject }.to raise_error(MicrosoftSync::InvalidOrMissingLoginAttributeConfig)
end
end
context 'when microsoft sync is configured' do
before do
root_account.settings[:microsoft_sync_enabled] = true
root_account.settings[:microsoft_sync_tenant] = 'tenant.example.com'
end
context 'when the login_attribute=email' do
let(:email_address) { 'email@example.com' }
let(:communication_channel) do
communication_channel_model(workflow_state: :active, path: email_address)
end
let(:user_ids) { [communication_channel.user_id] }
before do
(1..3).each do |i|
communication_channel_model(workflow_state: :active,
path: "email_#{i}@example.com",
user_id: communication_channel.user_id)
end
root_account.settings[:microsoft_sync_login_attribute] = 'email'
root_account.save!
end
it 'returns an array mapping the user id with upn' do
users_upns = subject.to_h
expect(users_upns.size).to eq 1
expect(users_upns[communication_channel.user_id]).to eq email_address
end
end
context 'when the login_attribute=preferred_username' do
let(:preferred_username) { 'preferred_username@example.com' }
let(:user) do
user_with_pseudonym(username: preferred_username)
end
let(:user_ids) { [user.id] }
before do
3.times.each { pseudonym(user) }
root_account.settings[:microsoft_sync_login_attribute] = 'preferred_username'
root_account.save!
end
it 'returns an array mapping the user id with upn' do
users_upns = subject.to_h
expect(users_upns.size).to eq 1
expect(users_upns[user.id]).to eq preferred_username
end
end
context 'when the login_attribute=sis_user_id' do
let(:sis_user_id) { '1021616' }
let(:user) do
user_with_pseudonym(sis_user_id: sis_user_id)
end
let(:user_ids) { [user.id] }
before do
(1..3).each { |i| pseudonym(user, sis_user_id: "#{sis_user_id}#{i}") }
root_account.settings[:microsoft_sync_login_attribute] = 'sis_user_id'
root_account.save!
end
it 'returns an array mapping the user id with upn' do
users_upns = subject.to_h
expect(users_upns.size).to eq 1
expect(users_upns[user.id]).to eq sis_user_id
end
end
context 'when the login_attribute=invalid' do
let(:user_ids) { [1] }
before do
root_account.settings[:microsoft_sync_login_attribute] = 'invalid'
root_account.save!
end
it 'raise an error' do
expect { subject }.to raise_error(MicrosoftSync::InvalidOrMissingLoginAttributeConfig)
end
end
end
end
end