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:
parent
8e88be86eb
commit
60c1820b8f
|
@ -126,8 +126,8 @@ module MicrosoftSync
|
||||||
MicrosoftSync::UserMapping.find_enrolled_user_ids_without_mappings(
|
MicrosoftSync::UserMapping.find_enrolled_user_ids_without_mappings(
|
||||||
course: course, batch_size: ENROLLMENTS_UPN_FETCHING_BATCH_SIZE
|
course: course, batch_size: ENROLLMENTS_UPN_FETCHING_BATCH_SIZE
|
||||||
) do |user_ids|
|
) do |user_ids|
|
||||||
users_and_upns = CommunicationChannel.
|
users_upns_finder = MicrosoftSync::UsersUpnsFinder.new(user_ids, group.root_account)
|
||||||
where(user_id: user_ids, path_type: 'email').pluck(:user_id, :path)
|
users_and_upns = users_upns_finder.call
|
||||||
|
|
||||||
users_and_upns.each_slice(GraphServiceHelpers::USERS_UPNS_TO_AADS_BATCH_SIZE) do |slice|
|
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))
|
upn_to_aad = graph_service_helpers.users_upns_to_aads(slice.map(&:last))
|
||||||
|
|
|
@ -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
|
|
@ -72,6 +72,7 @@ describe MicrosoftSync::SyncerSteps do
|
||||||
ra = course.root_account
|
ra = course.root_account
|
||||||
ra.settings[:microsoft_sync_enabled] = sync_enabled
|
ra.settings[:microsoft_sync_enabled] = sync_enabled
|
||||||
ra.settings[:microsoft_sync_tenant] = tenant
|
ra.settings[:microsoft_sync_tenant] = tenant
|
||||||
|
ra.settings[:microsoft_sync_login_attribute] = 'email'
|
||||||
ra.save!
|
ra.save!
|
||||||
|
|
||||||
allow(MicrosoftSync::GraphServiceHelpers).to \
|
allow(MicrosoftSync::GraphServiceHelpers).to \
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue