588 lines
21 KiB
Ruby
588 lines
21 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#
|
|
# Copyright (C) 2011 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 File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
|
|
|
|
describe "AuthenticationProviders API", type: :request do
|
|
before :once do
|
|
@account = account_model(:name => 'root')
|
|
user_with_pseudonym(:active_all => true, :account => @account)
|
|
@account.authentication_providers.scope.delete_all
|
|
@account.account_users.create!(user: @user)
|
|
@cas_hash = { "auth_type" => "cas",
|
|
"auth_base" => "127.0.0.1",
|
|
"jit_provisioning" => false }
|
|
@saml_hash = { 'auth_type' => 'saml',
|
|
'idp_entity_id' => 'http://example.com/saml1',
|
|
'log_in_url' => 'http://example.com/saml1/sli',
|
|
'log_out_url' => 'http://example.com/saml1/slo',
|
|
'certificate_fingerprint' => '111222',
|
|
'identifier_format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
|
'federated_attributes' => {},
|
|
'jit_provisioning' => false }
|
|
@ldap_hash = { 'auth_type' => 'ldap',
|
|
'auth_host' => '127.0.0.1',
|
|
'auth_filter' => 'filter1',
|
|
'auth_username' => 'username1',
|
|
'auth_password' => 'password1',
|
|
'jit_provisioning' => false }
|
|
end
|
|
|
|
context "/index" do
|
|
def call_index(status = 200)
|
|
api_call(:get, "/api/v1/accounts/#{@account.id}/authentication_providers",
|
|
{ :controller => 'authentication_providers', :action => 'index', :account_id => @account.id.to_s, :format => 'json' },
|
|
{}, {}, :expected_status => status)
|
|
end
|
|
|
|
it "returns all aacs in position order" do
|
|
@account.authentication_providers.create!(@saml_hash.merge(:idp_entity_id => "a"))
|
|
@account.authentication_providers.create!(@saml_hash.merge(:idp_entity_id => "d"))
|
|
config3 = @account.authentication_providers.create!(@saml_hash.merge(:idp_entity_id => "r"))
|
|
config3.move_to_top
|
|
config3.save!
|
|
|
|
res = call_index
|
|
|
|
expect(res.map { |c| c['idp_entity_id'] }.join).to eq 'rad'
|
|
end
|
|
|
|
it "returns unauthorized error" do
|
|
course_with_student(:course => @course)
|
|
call_index(401)
|
|
end
|
|
end
|
|
|
|
context "/create" do
|
|
# the deprecated mass-update/create is tested in account_authorization_configs_deprecated_api_spec.rb
|
|
|
|
def call_create(params, status = 200)
|
|
json = api_call(:post, "/api/v1/accounts/#{@account.id}/authentication_providers",
|
|
{ :controller => 'authentication_providers', :action => 'create', :account_id => @account.id.to_s, :format => 'json' },
|
|
params, {}, :expected_status => status)
|
|
@account.reload
|
|
json
|
|
end
|
|
|
|
it "creates a saml aac" do
|
|
json = call_create(@saml_hash)
|
|
aac = @account.authentication_providers.first
|
|
expect(aac.auth_type).to eq 'saml'
|
|
expect(aac.idp_entity_id).to eq 'http://example.com/saml1'
|
|
expect(aac.log_in_url).to eq 'http://example.com/saml1/sli'
|
|
expect(aac.log_out_url).to eq 'http://example.com/saml1/slo'
|
|
expect(aac.certificate_fingerprint).to eq '111222'
|
|
expect(aac.identifier_format).to eq 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
|
|
expect(aac.position).to eq 1
|
|
end
|
|
|
|
it "can create an initially mfa-required provider" do
|
|
@account.settings[:mfa_settings] = :optional
|
|
@account.save!
|
|
@saml_hash[:mfa_required] = true
|
|
call_create(@saml_hash)
|
|
ap = @account.authentication_providers.first
|
|
expect(ap).to be_mfa_required
|
|
end
|
|
|
|
it "ignores mfa_required if the account doesn't have it enabled" do
|
|
@saml_hash[:mfa_required] = true
|
|
call_create(@saml_hash)
|
|
ap = @account.authentication_providers.first
|
|
expect(ap).not_to be_mfa_required
|
|
end
|
|
|
|
it "works with rails form style params" do
|
|
call_create({ :authentication_provider => @saml_hash })
|
|
aac = @account.authentication_providers.first
|
|
expect(aac.auth_type).to eq 'saml'
|
|
expect(aac.idp_entity_id).to eq 'http://example.com/saml1'
|
|
end
|
|
|
|
it "creates multiple saml aacs" do
|
|
call_create(@saml_hash)
|
|
call_create(@saml_hash.merge('idp_entity_id' => "secondeh"))
|
|
|
|
aac1 = @account.authentication_providers.first
|
|
expect(aac1.idp_entity_id).to eq 'http://example.com/saml1'
|
|
expect(aac1.position).to eq 1
|
|
|
|
aac2 = @account.authentication_providers.last
|
|
expect(aac2.idp_entity_id).to eq 'secondeh'
|
|
expect(aac2.position).to eq 2
|
|
end
|
|
|
|
it "creates an ldap aac" do
|
|
call_create(@ldap_hash)
|
|
aac = @account.authentication_providers.first
|
|
expect(aac.auth_type).to eq 'ldap'
|
|
expect(aac.auth_host).to eq '127.0.0.1'
|
|
expect(aac.auth_filter).to eq 'filter1'
|
|
expect(aac.auth_username).to eq 'username1'
|
|
expect(aac.auth_decrypted_password).to eq 'password1'
|
|
expect(aac.position).to eq 1
|
|
end
|
|
it "creates multiple ldap aacs" do
|
|
call_create(@ldap_hash)
|
|
call_create(@ldap_hash.merge('auth_host' => '127.0.0.2'))
|
|
aac = @account.authentication_providers.first
|
|
expect(aac.auth_host).to eq '127.0.0.1'
|
|
expect(aac.position).to eq 1
|
|
aac2 = @account.authentication_providers.last
|
|
expect(aac2.auth_host).to eq '127.0.0.2'
|
|
expect(aac2.position).to eq 2
|
|
end
|
|
it "defaults ldap auth_over_tls to 'start_tls'" do
|
|
call_create(@ldap_hash)
|
|
expect(@account.authentication_providers.first.auth_over_tls).to eq 'start_tls'
|
|
end
|
|
|
|
it "creates a cas aac" do
|
|
call_create(@cas_hash)
|
|
|
|
aac = @account.authentication_providers.first
|
|
expect(aac.auth_type).to eq 'cas'
|
|
expect(aac.auth_base).to eq '127.0.0.1'
|
|
expect(aac.position).to eq 1
|
|
end
|
|
|
|
it "does not error when mixing auth_types (for now)" do
|
|
call_create(@ldap_hash)
|
|
call_create(@saml_hash, 200)
|
|
end
|
|
|
|
it "updates positions" do
|
|
call_create(@ldap_hash)
|
|
call_create(@ldap_hash.merge('auth_host' => '127.0.0.2', 'position' => 1))
|
|
|
|
expect(@account.authentication_providers.first.auth_host).to eq '127.0.0.2'
|
|
|
|
call_create(@ldap_hash.merge('auth_host' => '127.0.0.3', 'position' => 2))
|
|
|
|
expect(@account.authentication_providers[0].auth_host).to eq '127.0.0.2'
|
|
expect(@account.authentication_providers[1].auth_host).to eq '127.0.0.3'
|
|
expect(@account.authentication_providers[2].auth_host).to eq '127.0.0.1'
|
|
end
|
|
|
|
it "errors if empty post params sent" do
|
|
json = call_create({}, 400)
|
|
expect(json['errors'].first).to eq(
|
|
{
|
|
'message' =>
|
|
"invalid or missing auth_type '', must be one of #{
|
|
AuthenticationProvider.valid_auth_types.join(',')
|
|
}"
|
|
}
|
|
)
|
|
end
|
|
|
|
it 'returns bad request for invalid auth type' do
|
|
json = call_create({ auth_type: 'invalid' }, 400)
|
|
expect(json['errors'].first).to eq(
|
|
{
|
|
'message' =>
|
|
"invalid or missing auth_type 'invalid', must be one of #{
|
|
AuthenticationProvider.valid_auth_types.join(',')
|
|
}"
|
|
}
|
|
)
|
|
end
|
|
|
|
it "returns unauthorized error" do
|
|
course_with_student(:course => @course)
|
|
call_create({}, 401)
|
|
end
|
|
|
|
it "disables open registration when setting delegated auth" do
|
|
@account.settings = { open_registration: true }
|
|
@account.save!
|
|
call_create(@cas_hash)
|
|
expect(@account.open_registration?).to be_falsey
|
|
end
|
|
|
|
it "does not allow creation of duplicate singleton providers" do
|
|
call_create({ auth_type: 'facebook' })
|
|
call_create({ auth_type: 'facebook' }, 422)
|
|
end
|
|
end
|
|
|
|
describe "/update" do
|
|
before :once do
|
|
@aac = @account.authentication_providers.create!(@saml_hash)
|
|
end
|
|
|
|
it "allows updating without auth type" do
|
|
json = api_call(:put, "/api/v1/accounts/#{@account.id}/authentication_providers/#{@aac.id}",
|
|
{ controller: 'authentication_providers',
|
|
action: 'update',
|
|
account_id: @account.id.to_s,
|
|
id: @aac.to_param,
|
|
format: 'json' },
|
|
{ authentication_provider: { log_in_url: 'http://example.com/updated_cool_log_in' } })
|
|
expect(json['log_in_url']).to eq 'http://example.com/updated_cool_log_in'
|
|
end
|
|
|
|
it "errors when changing the type" do
|
|
json = api_call(:put, "/api/v1/accounts/#{@account.id}/authentication_providers/#{@aac.id}",
|
|
{ controller: 'authentication_providers',
|
|
action: 'update',
|
|
account_id: @account.id.to_s,
|
|
id: @aac.to_param,
|
|
format: 'json' },
|
|
{ authentication_provider: { log_in_url: 'http://example.com/updated_cool_log_in', auth_type: 'facebook' } },
|
|
{},
|
|
expected_status: 400)
|
|
expect(json['message']).to eq 'Can not change type of authorization config, please delete and create new config.'
|
|
end
|
|
end
|
|
|
|
context "/show" do
|
|
def call_show(id, status = 200)
|
|
api_call(:get, "/api/v1/accounts/#{@account.id}/authentication_providers/#{id}",
|
|
{ :controller => 'authentication_providers', :action => 'show', :account_id => @account.id.to_s, :id => id.to_param, :format => 'json' },
|
|
{}, {}, :expected_status => status)
|
|
end
|
|
|
|
it "returns saml aac" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
json = call_show(aac.id)
|
|
|
|
@saml_hash['id'] = aac.id
|
|
@saml_hash['position'] = 1
|
|
@saml_hash['login_handle_name'] = nil
|
|
@saml_hash['change_password_url'] = nil
|
|
@saml_hash['requested_authn_context'] = nil
|
|
@saml_hash['login_attribute'] = 'NameID'
|
|
@saml_hash['unknown_user_url'] = nil
|
|
@saml_hash['parent_registration'] = false
|
|
@saml_hash['metadata_uri'] = nil
|
|
@saml_hash['sig_alg'] = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
|
@saml_hash['strip_domain_from_login_attribute'] = false
|
|
@saml_hash['mfa_required'] = false
|
|
expect(json).to eq @saml_hash
|
|
end
|
|
|
|
it "returns ldap aac" do
|
|
aac = @account.authentication_providers.create!(@ldap_hash)
|
|
json = call_show(aac.id)
|
|
|
|
@ldap_hash.delete 'auth_password'
|
|
@ldap_hash['id'] = aac.id
|
|
@ldap_hash['auth_port'] = nil
|
|
@ldap_hash['auth_base'] = nil
|
|
@ldap_hash['auth_over_tls'] = 'start_tls'
|
|
@ldap_hash['identifier_format'] = nil
|
|
@ldap_hash['position'] = 1
|
|
@ldap_hash['mfa_required'] = false
|
|
expect(json).to eq @ldap_hash
|
|
end
|
|
|
|
it "returns cas aac" do
|
|
aac = @account.authentication_providers.create!(@cas_hash)
|
|
json = call_show(aac.id)
|
|
|
|
@cas_hash['log_in_url'] = nil
|
|
@cas_hash['id'] = aac.id
|
|
@cas_hash['position'] = 1
|
|
@cas_hash['unknown_user_url'] = nil
|
|
@cas_hash['federated_attributes'] = {}
|
|
@cas_hash['mfa_required'] = false
|
|
expect(json).to eq @cas_hash
|
|
end
|
|
|
|
it "404s" do
|
|
call_show(0, 404)
|
|
end
|
|
|
|
it "returns unauthorized error" do
|
|
course_with_student(:course => @course)
|
|
call_show(0, 401)
|
|
end
|
|
|
|
it "allows seeing the canvas auth type for any authenticated user" do
|
|
@account.authentication_providers.create!(auth_type: 'canvas')
|
|
course_with_student(:course => @course)
|
|
call_show('canvas')
|
|
end
|
|
end
|
|
|
|
context "/update" do
|
|
def call_update(id, params, status = 200)
|
|
json = api_call(:put, "/api/v1/accounts/#{@account.id}/authentication_providers/#{id}",
|
|
{ :controller => 'authentication_providers', :action => 'update', :account_id => @account.id.to_s, :id => id.to_param, :format => 'json' },
|
|
params, {}, :expected_status => status)
|
|
@account.reload
|
|
json
|
|
end
|
|
|
|
it "updates a saml aac" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
@saml_hash['idp_entity_id'] = 'hahahaha'
|
|
call_update(aac.id, @saml_hash)
|
|
|
|
aac.reload
|
|
expect(aac.idp_entity_id).to eq 'hahahaha'
|
|
end
|
|
|
|
it "returns error when it fails to update" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
@saml_hash['metadata_uri'] = 'hahahaha_super_invalid'
|
|
json = call_update(aac.id, @saml_hash, 422)
|
|
expect(json['errors'].first['field']).to eq 'metadata_uri'
|
|
end
|
|
|
|
it "updates federated attributes" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
json = call_update(aac.id, 'auth_type' => 'saml',
|
|
'federated_attributes' => { 'integration_id' => 'internal_id' })
|
|
# jit provisioning off; short form output
|
|
expect(json['federated_attributes']).to eq('integration_id' => 'internal_id')
|
|
aac.reload
|
|
expect(aac.federated_attributes).to eq('integration_id' => { 'attribute' => 'internal_id',
|
|
'provisioning_only' => false })
|
|
end
|
|
|
|
it "works with rails form style params" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
@saml_hash['idp_entity_id'] = 'hahahaha'
|
|
call_update(aac.id, { :authentication_provider => @saml_hash })
|
|
|
|
aac.reload
|
|
expect(aac.idp_entity_id).to eq 'hahahaha'
|
|
end
|
|
|
|
it "updates an ldap aac" do
|
|
aac = @account.authentication_providers.create!(@ldap_hash)
|
|
@ldap_hash['auth_host'] = '192.168.0.1'
|
|
call_update(aac.id, @ldap_hash)
|
|
|
|
aac.reload
|
|
expect(aac.auth_host).to eq '192.168.0.1'
|
|
end
|
|
|
|
it "updates a cas aac" do
|
|
aac = @account.authentication_providers.create!(@cas_hash)
|
|
@cas_hash['auth_base'] = '192.168.0.1'
|
|
call_update(aac.id, @cas_hash)
|
|
|
|
aac.reload
|
|
expect(aac.auth_base).to eq '192.168.0.1'
|
|
end
|
|
|
|
it "errors when mixing auth_types" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
json = call_update(aac.id, @cas_hash, 400)
|
|
expect(json['message']).to eq 'Can not change type of authorization config, please delete and create new config.'
|
|
end
|
|
|
|
it "updates positions" do
|
|
@account.authentication_providers.create!(@ldap_hash)
|
|
@ldap_hash['auth_host'] = '192.168.0.1'
|
|
aac2 = @account.authentication_providers.create!(@ldap_hash)
|
|
@ldap_hash['position'] = 1
|
|
call_update(aac2.id, @ldap_hash)
|
|
|
|
expect(@account.authentication_providers.first.id).to eq aac2.id
|
|
end
|
|
|
|
it "404s" do
|
|
call_update(0, {}, 404)
|
|
end
|
|
|
|
it "returns unauthorized error" do
|
|
course_with_student(:course => @course)
|
|
call_update(0, {}, 401)
|
|
end
|
|
|
|
it "can disable MFA" do
|
|
@account.settings[:mfa_settings] = :optional
|
|
@account.save!
|
|
aac = @account.authentication_providers.new(@cas_hash)
|
|
aac.mfa_required = true
|
|
aac.save!
|
|
@cas_hash['mfa_required'] = '0'
|
|
call_update(aac.id, @cas_hash)
|
|
|
|
aac.reload
|
|
expect(aac).not_to be_mfa_required
|
|
end
|
|
end
|
|
|
|
context "/destroy" do
|
|
def call_destroy(id, status = 200)
|
|
json = api_call(:delete, "/api/v1/accounts/#{@account.id}/authentication_providers/#{id}",
|
|
{ :controller => 'authentication_providers', :action => 'destroy', :account_id => @account.id.to_s, :id => id.to_param, :format => 'json' },
|
|
{}, {}, :expected_status => status)
|
|
@account.reload
|
|
json
|
|
end
|
|
|
|
it "deletes" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
call_destroy(aac.id)
|
|
|
|
expect(@account.non_canvas_auth_configured?).to be_falsey
|
|
end
|
|
|
|
it "repositions correctly" do
|
|
aac = @account.authentication_providers.create!(@saml_hash)
|
|
aac2 = @account.authentication_providers.create!(@saml_hash)
|
|
aac3 = @account.authentication_providers.create!(@saml_hash)
|
|
aac4 = @account.authentication_providers.create!(@saml_hash)
|
|
|
|
call_destroy(aac.id)
|
|
aac2.reload
|
|
aac3.reload
|
|
aac4.reload
|
|
expect(@account.authentication_providers.active.count).to eq 3
|
|
expect(@account.authentication_providers.active.first.id).to eq aac2.id
|
|
expect(aac2.position).to eq 1
|
|
expect(aac3.position).to eq 2
|
|
expect(aac4.position).to eq 3
|
|
|
|
call_destroy(aac3.id)
|
|
aac2.reload
|
|
aac4.reload
|
|
expect(@account.authentication_providers.active.count).to eq 2
|
|
expect(@account.authentication_providers.active.first.id).to eq aac2.id
|
|
expect(aac2.position).to eq 1
|
|
expect(aac4.position).to eq 2
|
|
end
|
|
|
|
it "404s" do
|
|
call_destroy(0, 404)
|
|
end
|
|
|
|
it "returns unauthorized error" do
|
|
course_with_student(:course => @course)
|
|
call_destroy(0, 401)
|
|
end
|
|
end
|
|
|
|
describe "sso settings" do
|
|
let(:sso_path) do
|
|
"/api/v1/accounts/#{@account.id}/sso_settings"
|
|
end
|
|
|
|
def update_settings(settings, expected_status)
|
|
api_call(:put,
|
|
sso_path,
|
|
{
|
|
controller: 'authentication_providers',
|
|
action: 'update_sso_settings',
|
|
account_id: @account.id.to_s,
|
|
format: 'json'
|
|
},
|
|
settings,
|
|
{},
|
|
expected_status: expected_status)
|
|
end
|
|
|
|
it "requires authorization" do
|
|
course_with_student(course: @course)
|
|
update_settings({}, 401)
|
|
end
|
|
|
|
it "sets auth settings" do
|
|
payload = {
|
|
'sso_settings' => {
|
|
'auth_discovery_url' => 'https://www.discover.com'
|
|
}
|
|
}
|
|
update_settings(payload, 200)
|
|
expect(@account.reload.auth_discovery_url).to eq('https://www.discover.com')
|
|
end
|
|
|
|
it "ignores settings that don't exist" do
|
|
payload = {
|
|
'sso_settings' => {
|
|
'abcdefg' => 'balongna'
|
|
}
|
|
}
|
|
update_settings(payload, 200)
|
|
end
|
|
|
|
context "with login handle pre-existing on account" do
|
|
before do
|
|
@account.login_handle_name = "LoginHandleSet"
|
|
@account.save!
|
|
end
|
|
|
|
it "clears settings with a key but no value" do
|
|
payload = {
|
|
'sso_settings' => {
|
|
'login_handle_name' => ''
|
|
}
|
|
}
|
|
update_settings(payload, 200)
|
|
expect(@account.reload.login_handle_name).to be_nil
|
|
end
|
|
|
|
it "leaves unspecified settings alone" do
|
|
payload = {
|
|
'sso_settings' => {
|
|
'auth_discovery_url' => 'someurl'
|
|
}
|
|
}
|
|
update_settings(payload, 200)
|
|
expect(@account.reload.login_handle_name).to eq("LoginHandleSet")
|
|
end
|
|
|
|
it "can get the current state of settings" do
|
|
response = api_call(:get,
|
|
sso_path,
|
|
{
|
|
controller: "authentication_providers",
|
|
action: "show_sso_settings",
|
|
account_id: @account.id.to_s,
|
|
format: 'json'
|
|
}, {}, {},
|
|
expected_status: 200)
|
|
|
|
expect(response['sso_settings']['login_handle_name'])
|
|
.to eq("LoginHandleSet")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "API JSON" do
|
|
describe 'federated_attributes' do
|
|
it 'excludes provisioning only attributes when jit_provisioning is off' do
|
|
aac = AuthenticationProvider::SAML.new(
|
|
federated_attributes: { 'integration_id' => { 'attribute' => 'internal_id' },
|
|
'sis_user_id' => { 'attribute' => 'external_id',
|
|
'provisioning_only' => true } }
|
|
)
|
|
expect(aac.federated_attributes_for_api).to eq('integration_id' => 'internal_id')
|
|
end
|
|
|
|
it 'uses full form when jit_provisioning is on' do
|
|
federated_attributes = { 'integration_id' => { 'attribute' => 'internal_id',
|
|
'provisioning_only' => false },
|
|
'sis_user_id' => { 'attribute' => 'external_id',
|
|
'provisioning_only' => true } }
|
|
aac = AuthenticationProvider::SAML.new(federated_attributes: federated_attributes,
|
|
jit_provisioning: true)
|
|
expect(aac.federated_attributes_for_api).to eq(federated_attributes)
|
|
end
|
|
end
|
|
end
|
|
end
|