Show Account settings and edit MSFT Sync settings

Added an endpoint that allows you to view the specified accounts
settings, as long as you're authorized.

In addition, update the update_api action to allow updating Microsoft Teams
Sync settings, namely :microsoft_sync_enabled, :microsoft_sync_tenant,
and :microsoft_sync_login_attribute.

closes INTEROP-6631, INTEROP-6630

test-plan:
- Ensure you have an authorization token with AccountAdmin privileges or
  higher.
- Using curl/Postman/Insomnia, try to enable Microsoft Sync with the
  feature flag disabled. Ensure a 400 is returned.
- Enable the :microsoft_group_enrollments_syncing feature flag
- Try to enable Microsoft Sync without a tenant or login_attribute and
  ensure a 400 is returned. Gotta have that info
- Try to enable Microsoft Sync with an invalid login_attribute. Choose
  any random string at all. Ensure you get a 400.
- try to enable Microsoft Sync without a valid tenant/domain name, such
  as "://$$$$$", or "invalidtenant-". Ensure a 400 is returned.
- Enable Microsoft Sync with a valid tenant and login_attribute. Ensure
  a 200 is returned.
- Try and modify settings as an unauthenticated user. You should get
  back a 401.
- To test the show settings endpoint, make sure you have some account
  settings set.
- Try to access the account settings as an unauthenticated user. Make
  sure you get a 401.
- Access the account settings as an authorized user (Account Admin).
  Ensure that you get back a JSON object that represents all of your
  current account settings.

flag = microsoft_group_enrollments_syncing

Change-Id: Ib785987621e090a80ffa63fb48be3ed63243fe56
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/261189
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Mysti Lilla <mysti@instructure.com>
Product-Review: Ryan Hawkins <ryan.hawkins@instructure.com>
Reviewed-by: Evan Battaglia <ebattaglia@instructure.com>
This commit is contained in:
Ryan Hawkins 2021-03-19 16:45:15 -06:00
parent 9ba2fd8b13
commit 1e214ae35e
5 changed files with 336 additions and 5 deletions

View File

@ -302,6 +302,7 @@ class AccountsController < ApplicationController
include Api::V1::Account
include CustomSidebarLinksHelper
include SupportHelpers::ControllerHelpers
include MicrosoftSync::Concerns::Settings
INTEGER_REGEX = /\A[+-]?\d+\z/
SIS_ASSINGMENT_NAME_LENGTH_DEFAULT = 255
@ -397,6 +398,22 @@ class AccountsController < ApplicationController
end
end
# @API Settings
# Returns all of the settings for the specified account as a JSON object. The caller must be an Account
# admin with the manage_account_settings permission.
#
# @example_request
# curl https://<canvas>/api/v1/accounts/<account_id>/settings \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {"usage_rights_required": true, "lock_all_announcements": true, "restrict_student_past_view": true}
def show_settings
return render_unauthorized_action unless @account.grants_right?(@current_user, session, :manage_account_settings)
render :json => @account.settings
end
# @API Permissions
# Returns permission information for the calling user and the given account.
# You may use `self` as the account id to check permissions against the domain root account.
@ -790,6 +807,13 @@ class AccountsController < ApplicationController
end
end
# All the Microsoft Teams Sync things!
sync_enabled = params.dig(:account, :settings)&.delete(:microsoft_sync_enabled)
tenant = params.dig(:account, :settings)&.delete(:microsoft_sync_tenant)
login_attribute = params.dig(:account, :settings)&.delete(:microsoft_sync_login_attribute)
set_microsoft_sync_settings(sync_enabled, tenant, login_attribute)
# quotas (:manage_account_quotas)
quota_settings = account_params.slice(:default_storage_quota_mb, :default_user_storage_quota_mb,
:default_group_storage_quota_mb)
@ -870,6 +894,21 @@ class AccountsController < ApplicationController
# @argument account[settings][restrict_student_future_view][value] [Boolean]
# Restrict students from viewing courses before start date
#
# @argument account[settings][microsoft_sync_enabled] [Boolean]
# Determines whether this account has Microsoft Teams Sync enabled or not.
#
# Note that if you are altering Microsoft Teams sync settings you must enable
# the Microsoft Group enrollment syncing feature flag. In addition, if you are enabling
# Microsoft Teams sync, you must also specify a tenant and login attribute.
#
# @argument account[settings][microsoft_sync_tenant]
# The tenant this account should use when using Microsoft Teams Sync.
# This should be an Azure Active Directory domain name.
#
# @argument account[settings][microsoft_sync_login_attribute]
# The attribute this account should use to lookup users when using Microsoft Teams Sync.
# Must be one of sub, email, oid, or preferred_username.
#
# @argument account[settings][restrict_student_future_view][locked] [Boolean]
# Lock this setting for sub-accounts and courses
#
@ -1624,10 +1663,11 @@ class AccountsController < ApplicationController
:include_students_in_global_survey, :license_type,
{:lock_all_announcements => [:value, :locked]}.freeze,
:login_handle_name, :mfa_settings, :no_enrollments_can_create_courses,
:mobile_qr_login_is_enabled, :open_registration,
:outgoing_email_default_name, :prevent_course_renaming_by_teachers,
:prevent_course_availability_editing_by_teachers, :restrict_quiz_questions,
{:restrict_student_future_listing => [:value, :locked].freeze}.freeze,
:mobile_qr_login_is_enabled,
:microsoft_sync_enabled, :microsoft_sync_tenant, :microsoft_sync_login_attribute,
:open_registration, :outgoing_email_default_name, :prevent_course_availability_editing_by_teachers,
:prevent_course_renaming_by_teachers, :restrict_quiz_questions,
{:restrict_student_future_listing => [:value, :locked]}.freeze,
{:restrict_student_future_view => [:value, :locked]}.freeze,
{:restrict_student_past_view => [:value, :locked]}.freeze,
:self_enrollment, :show_scheduler, :sis_app_token, :sis_app_url,

View File

@ -0,0 +1,85 @@
# 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/>.
module MicrosoftSync::Concerns
module Settings
extend ActiveSupport::Concern
VALID_SYNC_LOGIN_ATTRIBUTES = %w(sub email oid preferred_username).freeze
def set_microsoft_sync_settings(enabled, tenant, login_attribute)
return if enabled.nil? && tenant.blank? && login_attribute.blank?
return unless valid_settings?(enabled, tenant, login_attribute)
@account.settings[:microsoft_sync_enabled] = format_enabled(enabled)
@account.settings[:microsoft_sync_tenant] = format_tenant(tenant)
@account.settings[:microsoft_sync_login_attribute] = format_login_attribute(login_attribute)
end
def format_enabled(sync_enabled)
ActiveModel::Type::Boolean.new.cast(sync_enabled)
end
# A valid tenant is effectively a domain name (ex: canvastest2.onmicrosoft.com), consisting
# of alphanumeric characters, with the restriction that each subdomain cannot start or end with
# a hyphen. Normally we would use something like URI.parse(tenant), but that allows domains
# that aren't valid, so we had to make a custom regex.
def format_tenant(tenant)
return nil if tenant.blank?
tenant = tenant.strip
# Uses look ahead and look behind to ensure we don't start/end with hyphens in any subdomains/TLD
regex = /^((?!-)[A-Za-z0-9-]+(?<!-)\.)+(?!-)[A-Za-z0-9-]+(?<!-)$/
return tenant if tenant =~ regex
nil
end
def format_login_attribute(login_attr)
return nil unless VALID_SYNC_LOGIN_ATTRIBUTES.include?(login_attr)
login_attr
end
# Checks if the passed settings are valid, and adds error messages as appropriate
def valid_settings?(enabled, tenant, attribute)
unless @account.root_account.feature_enabled?(:microsoft_group_enrollments_syncing)
@account.errors.add(:bad_request,
t("This account doesn't allow for Microsoft Teams sync to be enabled. Please enable the \"Microsoft Group enrollment syncing\" feature flag before editing any settings"))
return false
end
enabled = format_enabled(enabled)
if enabled.nil?
@account.errors.add(:bad_request, t("You must specify whether to enable or disable Microsoft Teams sync"))
false
elsif enabled && (tenant.blank? || attribute.blank?)
@account.errors.add(:bad_request, t("You must provide a tenant and login attribute to enabled Microsoft Teams sync"))
false
elsif enabled && format_tenant(tenant).blank?
@account.errors.add(:bad_request, t("Invalid Microsoft Sync tenant given. Please validate your tenant"))
false
elsif enabled && format_login_attribute(attribute).blank?
@account.errors.add(:bad_request,
t("Invalid Microsoft Teams Sync login attribute. Valid login attributes: %{valid_attributes}",
valid_attributes: VALID_SYNC_LOGIN_ATTRIBUTES.to_sentence(:or)))
false
end
true
end
end
end

View File

@ -245,6 +245,11 @@ class Account < ActiveRecord::Base
add_setting :global_includes, :root_only => true, :boolean => true, :default => false
add_setting :sub_account_includes, :boolean => true, :default => false
# Microsoft Sync Account Settings
add_setting :microsoft_sync_enabled, :root_only => true, :boolean => true, :default => false
add_setting :microsoft_sync_tenant, :root_only => true
add_setting :microsoft_sync_login_attribute, :root_only => true
# Help link settings
add_setting :custom_help_links, :root_only => true
add_setting :help_link_icon, :root_only => true

View File

@ -1496,6 +1496,7 @@ CanvasRails::Application.routes.draw do
get 'accounts/:account_id/sub_accounts', action: :sub_accounts, as: 'sub_accounts'
get 'accounts/:account_id/courses/:id', controller: :courses, action: :show, as: 'account_course_show'
get 'accounts/:account_id/permissions', action: :permissions
get 'accounts/:account_id/settings', action: :show_settings
delete 'accounts/:account_id/users/:user_id', action: :remove_user
put 'accounts/:account_id/users/:user_id/restore', action: :restore_user
end

View File

@ -517,6 +517,188 @@ describe "Accounts API", type: :request do
expect(@a1.settings).to be_empty
end
context 'Microsoft Teams Sync' do
let(:update_sync_settings_params) do
{
account: {
settings: {
microsoft_sync_enabled: sync_enabled,
microsoft_sync_tenant: tenant_name,
microsoft_sync_login_attribute: attribute
}.compact
}
}
end
let(:update_path) { "/api/v1/accounts/#{@a1.id}" }
let(:sync_enabled) { true }
let(:tenant_name) { "canvastest2.onmicrosoft.com" }
let(:attribute) { "sub" }
before(:each) do
user_session(@user)
end
context 'microsoft_group_enrollments_syncing flag disabled' do
before(:each) { @a1.disable_feature!(:microsoft_group_enrollments_syncing)}
it "shouldn't allow editing settings" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 400 })
@a1.reload
expect(@a1.settings.size).to be 0
end
context 'subaccounts' do
let(:update_path) { "/api/v1/accounts/#{@a2.id}" }
let(:header_options_hash) do
{
controller: 'accounts',
action: 'update',
id: @a2.to_param,
format: 'json'
}
end
it "shouldn't allow subaccounts to edit settings" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 400 })
@a2.reload
expect(@a2.settings.size).to be 0
end
end
end
context 'microsoft_group_enrollments_syncing flag enabled' do
before(:each) { @a1.enable_feature!(:microsoft_group_enrollments_syncing) }
context 'no tenant or login attribute provided' do
let(:tenant_name) { nil }
let(:attribute) { nil }
it "shouldn't allow enabling sync" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 400 })
@a1.reload
expect(@a1.settings.size).to be 0
end
end
context 'invalid tenant name supplied' do
let(:tenant_name) { '^&abcd.com' }
it "shouldn't allow enabling sync" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 400 })
@a1.reload
expect(@a1.settings.size).to be 0
end
end
context 'invalid login attribute' do
let(:attribute) { 'garbage' }
it "shouldn't allow invalid login attributes" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 400 })
@a1.reload
expect(@a1.settings.size).to be 0
end
end
context 'non-admin user' do
let(:generic_user) { user_factory }
it "can't update settings" do
api_call_as_user(generic_user, :put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 401 })
@a1.reload
expect(@a1.settings.size).to eq 0
end
end
context 'disabling sync' do
context('no tenant or login attribute specified') do
let(:sync_enabled) { false }
let(:tenant_name) { nil }
let(:attribute) { nil }
it "allows updating settings" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 200 })
@a1.reload
expect(@a1.settings).to eq update_sync_settings_params[:account][:settings]
end
end
it "allows specifying a tenant or login attribute" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 200 })
@a1.reload
expect(@a1.settings).to eq update_sync_settings_params[:account][:settings]
end
end
context 'admin user' do
it "should save valid settings" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 200 })
@a1.reload
expect(@a1.settings).to eq update_sync_settings_params[:account][:settings]
end
it 'should allow strings to be used for sync_enabled' do
update_sync_settings_params[:account][:settings][:microsoft_sync_enabled] = "true"
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 200 })
@a1.reload
expect(@a1.settings[:microsoft_sync_enabled]).to be true
expect(@a1.settings[:microsoft_sync_tenant]).to eq tenant_name
expect(@a1.settings[:microsoft_sync_login_attribute]).to eq attribute
end
it "should allow setting the login attribute to any of the allowed attributes" do
%w(sub email oid preferred_username).each do |attribute|
update_sync_settings_params[:account][:settings][:microsoft_sync_login_attribute] = attribute
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 200 })
@a1.reload
expect(@a1.settings).to eq update_sync_settings_params[:account][:settings]
end
end
context 'subaccounts' do
let(:update_path) { "/api/v1/accounts/#{@a2.id}" }
let(:header_options_hash) do
{
controller: 'accounts',
action: 'update',
id: @a2.to_param,
format: 'json'
}
end
it "should save valid settings" do
api_call(:put, update_path, header_options_hash,
update_sync_settings_params, { expected_result: 200 })
@a2.reload
expect(@a2.settings).to eq update_sync_settings_params[:account][:settings]
end
end
end
end
end
context 'with :manage_storage_quotas' do
before(:once) do
# remove the user from being an Admin
@ -649,7 +831,7 @@ describe "Accounts API", type: :request do
{ controller: 'accounts', action: 'update', id: @a2.to_param, format: 'json' },
{ account: { course_template_id: template.id }})
@a2.reload
expect(@a2.course_template).to eq template
expect(@a2.course_template).to eq template
end
it "returns unauthorized when you don't have permission to change it" do
@ -1398,6 +1580,24 @@ describe "Accounts API", type: :request do
end
end
context "show settings" do
let(:show_settings_path) { "/api/v1/accounts/#{@a1.id}/settings"}
let(:show_settings_header) { { controller: :accounts, action: :show_settings, account_id: @a1.to_param, format: :json} }
let(:generic_user) { user_factory }
it "shouldn't allow regular users to see settings" do
api_call_as_user(generic_user, :get, show_settings_path, show_settings_header, {}, { expected_status: 401 })
end
it "should allow account admins to see settings" do
@a1.settings = { :microsoft_sync_enabled => true, :microsoft_sync_tenant => "testtenant.com" }
@a1.save!
json = api_call(:get, show_settings_path, show_settings_header, {}, { expected_status: 200 })
expect(json).to eq(@a1.settings.with_indifferent_access)
end
end
context "account api extension" do
module MockPlugin
def self.extend_account_json(hash, account, user, session, includes)