configure SAML via metadata URI
fixes CNVS-28890, CNVS-28891 including auto-refresh, and optimizations for InCommon test plan: * use a metadata URI to configure a SAML provider; everything should populate * have your IdP change their metadata, then wait a day for the periodic job, or manually run AccountAuthorizationConfig::SAML::MetadataRefresher.refresh_providers * it should be updated automatically * set the URI to urn:mace:incommon, and fill in a entity id of a school in InCommon * it should populate automatically Change-Id: Ie508483da2ffa81dce3b98dbd5ae5c0b9e2ac878 Reviewed-on: https://gerrit.instructure.com/76989 QA-Review: Jeremy Putnam <jeremyp@instructure.com> Reviewed-by: Rob Orton <rob@instructure.com> Tested-by: Jenkins Product-Review: Cody Cutrer <cody@instructure.com>
This commit is contained in:
parent
f71a829b47
commit
7db873f5eb
|
@ -89,8 +89,8 @@ gem 'rotp', '1.6.1', require: false
|
|||
gem 'net-ldap', '0.10.1', require: false
|
||||
gem 'ruby-duration', '3.2.0', require: false
|
||||
gem 'ruby-saml-mod', '0.3.0'
|
||||
gem 'saml2', '1.0.6', require: false
|
||||
gem 'nokogiri-xmlsec-me-harder', '0.9.3pre', require: false, github: 'instructure/nokogiri-xmlsec-me-harder', ref: 'a39924b7483aee45171c77cb1110dd92de5c7bbe'
|
||||
gem 'saml2', '1.0.7', require: false
|
||||
gem 'nokogiri-xmlsec-me-harder', '0.9.3pre', require: false, github: 'instructure/nokogiri-xmlsec-me-harder', ref: '3236a249986413c6c399ef3477132a0af0410bb7'
|
||||
gem 'rubycas-client', '2.3.9', require: false
|
||||
gem 'rubyzip', '1.1.1', require: 'zip'
|
||||
gem 'safe_yaml', '1.0.4', require: false
|
||||
|
|
|
@ -441,6 +441,17 @@ class AccountAuthorizationConfigsController < ApplicationController
|
|||
# An XML document to parse as SAML metadata, and automatically populate idp_entity_id,
|
||||
# log_in_url, log_out_url, certificate_fingerprint, and identifier_format
|
||||
#
|
||||
# - metadata_uri [Optional]
|
||||
#
|
||||
# A URI to download the SAML metadata from, and automatically populate idp_entity_id,
|
||||
# log_in_url, log_out_url, certificate_fingerprint, and identifier_format. This URI
|
||||
# will also be saved, and the metadata periodically refreshed, automatically. If
|
||||
# the metadata contains multiple entities, also supply idp_entity_id to distinguish
|
||||
# which one you want (otherwise the only entity in the metadata will be inferred).
|
||||
# If you provide the URI 'urn:mace:incommon', the InCommon metadata aggregate will
|
||||
# be used instead, and additional validation checks will happen (including
|
||||
# validating that the metadata has been properly signed with the InCommon key).
|
||||
#
|
||||
# - idp_entity_id
|
||||
#
|
||||
# The SAML IdP's entity ID
|
||||
|
@ -557,10 +568,19 @@ class AccountAuthorizationConfigsController < ApplicationController
|
|||
end
|
||||
update_deprecated_account_settings_data(aac_data, account_config)
|
||||
|
||||
unless account_config.save
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:error] = account_config.errors.full_messages
|
||||
redirect_to(account_authentication_providers_path(@account))
|
||||
end
|
||||
format.json { raise ActiveRecord::RecordInvalid.new(account_config) }
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if position.present?
|
||||
account_config.insert_at(position.to_i)
|
||||
else
|
||||
account_config.save!
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
|
@ -599,7 +619,18 @@ class AccountAuthorizationConfigsController < ApplicationController
|
|||
end
|
||||
|
||||
deselect_parent_registration(data, aac)
|
||||
aac.update_attributes(data)
|
||||
aac.assign_attributes(data)
|
||||
|
||||
unless aac.save
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:error] = aac.errors.full_messages
|
||||
redirect_to(account_authentication_providers_path(@account))
|
||||
end
|
||||
format.json { raise ActiveRecord::RecordInvalid.new(account_config) }
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if position.present?
|
||||
aac.insert_at(position.to_i)
|
||||
|
|
|
@ -44,7 +44,8 @@ class AccountAuthorizationConfig::SAML < AccountAuthorizationConfig::Delegated
|
|||
:idp_entity_id,
|
||||
:parent_registration,
|
||||
:jit_provisioning,
|
||||
:metadata
|
||||
:metadata,
|
||||
:metadata_uri
|
||||
].freeze
|
||||
end
|
||||
|
||||
|
@ -55,6 +56,7 @@ class AccountAuthorizationConfig::SAML < AccountAuthorizationConfig::Delegated
|
|||
SENSITIVE_PARAMS = [:metadata].freeze
|
||||
|
||||
before_validation :set_saml_defaults
|
||||
before_validation :download_metadata
|
||||
validates_presence_of :entity_id
|
||||
|
||||
def auth_provider_filter
|
||||
|
@ -70,6 +72,40 @@ class AccountAuthorizationConfig::SAML < AccountAuthorizationConfig::Delegated
|
|||
self.requested_authn_context = nil if self.requested_authn_context.blank?
|
||||
end
|
||||
|
||||
def download_metadata
|
||||
return unless metadata_uri.present?
|
||||
return unless metadata_uri_changed? || idp_entity_id_changed?
|
||||
# someone's trying to cheat; switch to our more efficient implementation
|
||||
self.metadata_uri = InCommon::URN if metadata_uri == InCommon.endpoint
|
||||
|
||||
if metadata_uri == InCommon::URN
|
||||
unless idp_entity_id.present?
|
||||
errors.add(:idp_entity_id, :present)
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
entity = InCommon.metadata[idp_entity_id]
|
||||
unless entity
|
||||
errors.add(:idp_entity_id, t("Entity %{entity_id} not found in InCommon Metadata", entity_id: idp_entity_id))
|
||||
return
|
||||
end
|
||||
populate_from_metadata(entity)
|
||||
rescue => e
|
||||
::Canvas::Errors.capture_exception(:incommon, e)
|
||||
errors.add(:metadata_uri, e.message)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
populate_from_metadata_url(metadata_uri)
|
||||
rescue => e
|
||||
::Canvas::Errors.capture_exception(:saml_metadata_refresh, e)
|
||||
errors.add(:metadata_uri, e.message)
|
||||
end
|
||||
end
|
||||
|
||||
def self.login_attributes
|
||||
{
|
||||
'NameID' => 'nameid',
|
||||
|
@ -104,18 +140,23 @@ class AccountAuthorizationConfig::SAML < AccountAuthorizationConfig::Delegated
|
|||
|
||||
def populate_from_metadata_xml(xml)
|
||||
entity = SAML2::Entity.parse(xml)
|
||||
raise "Invalid schema" unless entity.valid_schema?
|
||||
if entity.is_a?(SAML2::Entity::Group) && idp_entity_id.present?
|
||||
entity = entity.find { |e| e.entity_id == idp_entity_id }
|
||||
end
|
||||
raise "Must be a single Entity" unless entity.is_a?(SAML2::Entity)
|
||||
populate_from_metadata(entity)
|
||||
end
|
||||
alias_method :metadata=, :populate_from_metadata_xml
|
||||
|
||||
def populate_from_metadata_url(url)
|
||||
response = ::Canvas.timeout_protection("saml_metadata_fetch") do
|
||||
CanvasHttp.get(url)
|
||||
::Canvas.timeout_protection("saml_metadata_fetch") do
|
||||
CanvasHttp.get(url) do |response|
|
||||
# raise error unless it's a 2xx
|
||||
response.value
|
||||
populate_from_metadata_xml(response.body)
|
||||
end
|
||||
end
|
||||
# raise error unless it's a 2xx
|
||||
response.value
|
||||
populate_from_metadata_xml(response.body)
|
||||
end
|
||||
|
||||
def saml_settings(current_host=nil)
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
#
|
||||
# Copyright (C) 2013 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 'saml2'
|
||||
|
||||
class AccountAuthorizationConfig::SAML::InCommon < AccountAuthorizationConfig::SAML::MetadataRefresher
|
||||
URN = 'urn:mace:incommon'.freeze
|
||||
|
||||
class << self
|
||||
def metadata
|
||||
if Canvas.redis_enabled?
|
||||
deflated = Canvas.redis.get('incommon_metadata')
|
||||
existing_data = Zlib::Inflate.inflate(deflated) if deflated
|
||||
end
|
||||
new_data = refresh_if_necessary('incommon', endpoint, force_fetch: !existing_data)
|
||||
validate_and_parse_metadata(new_data || existing_data)
|
||||
end
|
||||
|
||||
def refresh_providers(shard_scope: Shard.in_current_region, providers: nil)
|
||||
providers ||= AccountAuthorizationConfig::SAML.active.
|
||||
where(metadata_uri: URN).shard(shard_scope)
|
||||
|
||||
# don't even bother checking InCommon if no one is using it
|
||||
# (but a multi-shard environment probably is, and it's expensive
|
||||
# to check them all, so just check InCommon)
|
||||
return if Shard.count <= 1 && !providers.exists?
|
||||
|
||||
new_data = refresh_if_necessary('incommon', endpoint)
|
||||
# no changes; don't bother with the hard work
|
||||
return unless new_data
|
||||
|
||||
metadata = validate_and_parse_metadata(new_data)
|
||||
|
||||
providers.each do |provider|
|
||||
entity = metadata[provider.idp_entity_id]
|
||||
|
||||
unless entity
|
||||
::Canvas::Errors.capture_exception(:incommon,
|
||||
"Entity #{provider.idp_entity_id} not found in InCommon metadata")
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
provider.populate_from_metadata(entity)
|
||||
provider.save! if provider.changed?
|
||||
rescue => e
|
||||
::Canvas::Errors.capture_exception(:incommon, e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def endpoint
|
||||
Setting.get('incommon_metadata_url', 'http://md.incommon.org/InCommon/InCommon-metadata.xml')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_and_parse_metadata(xml)
|
||||
entities = SAML2::Entity.parse(xml)
|
||||
raise "Expected a group of entities" unless entities.is_a?(SAML2::Entity::Group)
|
||||
raise "Invalid XML" unless entities.valid_schema?
|
||||
unless entities.valid_until && entities.valid_until > Time.now.utc
|
||||
raise "Problem with validUntil: #{entities.valid_until}"
|
||||
end
|
||||
raise "Not signed!" unless entities.signed?
|
||||
unless entities.valid_signature?(cert: Rails.root.join("config/saml/inc-md-cert.pem").read)
|
||||
raise "Invalid signature!"
|
||||
end
|
||||
|
||||
entities.index_by(&:entity_id)
|
||||
end
|
||||
|
||||
def refresh_if_necessary(*args)
|
||||
result = super
|
||||
# save the new data if there is any
|
||||
if Canvas.redis_enabled? && result
|
||||
Canvas.redis.set('incommon_metadata', Zlib::Deflate.deflate(result, 9))
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,64 @@
|
|||
#
|
||||
# Copyright (C) 2013 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 'saml2'
|
||||
|
||||
class AccountAuthorizationConfig::SAML::MetadataRefresher
|
||||
class << self
|
||||
def refresh_providers(shard_scope: Shard.current, providers: nil)
|
||||
providers ||= AccountAuthorizationConfig::SAML.active.
|
||||
where.not(metadata_uri: [nil, AccountAuthorizationConfig::SAML::InCommon::URN]).
|
||||
shard(shard_scope)
|
||||
|
||||
providers.each do |provider|
|
||||
begin
|
||||
new_data = refresh_if_necessary(provider.global_id, provider.metadata_uri)
|
||||
next unless new_data
|
||||
provider.populate_from_metadata_xml(new_data)
|
||||
provider.save! if provider.changed?
|
||||
rescue => e
|
||||
::Canvas::Errors.capture_exception(:saml_metadata_refresh, e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# returns the new data if it changed, or false if it has not
|
||||
def refresh_if_necessary(provider_key, endpoint, force_fetch: false)
|
||||
if !force_fetch && Canvas.redis_enabled?
|
||||
etag = Canvas.redis.get("saml_#{provider_key}_etag")
|
||||
end
|
||||
|
||||
headers = {}
|
||||
headers['If-None-Match'] = etag if etag
|
||||
CanvasHttp.get(endpoint, headers) do |response|
|
||||
if response.is_a?(Net::HTTPNotModified)
|
||||
return false
|
||||
end
|
||||
# raise on non-success
|
||||
response.value
|
||||
# store new data
|
||||
if Canvas.redis_enabled? && response['ETag']
|
||||
Canvas.redis.set("saml_#{provider_key}_etag", response['ETag'])
|
||||
end
|
||||
return response.body
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,6 +4,16 @@ is available at that URL.
|
|||
TEXT
|
||||
%></p>
|
||||
<% css_bundle :saml_fields %>
|
||||
<p><%= t(<<-TEXT)
|
||||
Provide a URI to your IdP's metadata to automatically populate the other
|
||||
fields. If your school is part of InCommon, specify urn:mace:incommon for
|
||||
the metadata URI, and also provide your school's entity ID.
|
||||
TEXT
|
||||
%></p>
|
||||
<div class="ic-Form-control">
|
||||
<%= f.label :metadata_uri, t('IdP Metadata URI'), class: 'ic-Label' %>
|
||||
<%= f.text_field :metadata_uri, class: 'ic-Input' %>
|
||||
</div>
|
||||
<div class="ic-Form-control">
|
||||
<%= f.label :idp_entity_id, t('IdP Entity ID'), class: 'ic-Label' %>
|
||||
<%= f.text_field :idp_entity_id, class: 'ic-Input' %>
|
||||
|
|
|
@ -134,4 +134,17 @@ Rails.configuration.after_initialize do
|
|||
Delayed::Periodic.cron 'Version::Partitioner.process', '0 0 * * *' do
|
||||
with_each_shard_by_database(Version::Partitioner, :process)
|
||||
end
|
||||
|
||||
if AccountAuthorizationConfig::SAML.enabled?
|
||||
Delayed::Periodic.cron 'AccountAuthorizationConfig::SAML::MetadataRefresher.refresh_providers', '15 0 * * *' do
|
||||
with_each_shard_by_database(AccountAuthorizationConfig::SAML::MetadataRefresher,
|
||||
:refresh_providers)
|
||||
end
|
||||
|
||||
Delayed::Periodic.cron 'AccountAuthorizationConfig::SAML::InCommon.refresh_providers', '45 0 * * *' do
|
||||
DatabaseServer.send_in_each_region(AccountAuthorizationConfig::SAML::InCommon,
|
||||
:refresh_providers,
|
||||
singleton: 'AccountAuthorizationConfig::SAML::InCommon.refresh_providers')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDgTCCAmmgAwIBAgIJAJRJzvdpkmNaMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV
|
||||
BAYTAlVTMRUwEwYDVQQKDAxJbkNvbW1vbiBMTEMxMTAvBgNVBAMMKEluQ29tbW9u
|
||||
IEZlZGVyYXRpb24gTWV0YWRhdGEgU2lnbmluZyBLZXkwHhcNMTMxMjE2MTkzNDU1
|
||||
WhcNMzcxMjE4MTkzNDU1WjBXMQswCQYDVQQGEwJVUzEVMBMGA1UECgwMSW5Db21t
|
||||
b24gTExDMTEwLwYDVQQDDChJbkNvbW1vbiBGZWRlcmF0aW9uIE1ldGFkYXRhIFNp
|
||||
Z25pbmcgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Chdkrn+
|
||||
dG5Zj5L3UIw+xeWgNzm8ajw7/FyqRQ1SjD4Lfg2WCdlfjOrYGNnVZMCTfItoXTSp
|
||||
g4rXxHQsykeNiYRu2+02uMS+1pnBqWjzdPJE0od+q8EbdvE6ShimjyNn0yQfGyQK
|
||||
CNdYuc+75MIHsaIOAEtDZUST9Sd4oeU1zRjV2sGvUd+JFHveUAhRc0b+JEZfIEuq
|
||||
/LIU9qxm/+gFaawlmojZPyOWZ1JlswbrrJYYyn10qgnJvjh9gZWXKjmPxqvHKJcA
|
||||
TPhAh2gWGabWTXBJCckMe1hrHCl/vbDLCmz0/oYuoaSDzP6zE9YSA/xCplaHA0mo
|
||||
C1Vs2H5MOQGlewIDAQABo1AwTjAdBgNVHQ4EFgQU5ij9YLU5zQ6K75kPgVpyQ2N/
|
||||
lPswHwYDVR0jBBgwFoAU5ij9YLU5zQ6K75kPgVpyQ2N/lPswDAYDVR0TBAUwAwEB
|
||||
/zANBgkqhkiG9w0BAQsFAAOCAQEAaQkEx9xvaLUt0PNLvHMtxXQPedCPw5xQBd2V
|
||||
WOsWPYspRAOSNbU1VloY+xUkUKorYTogKUY1q+uh2gDIEazW0uZZaQvWPp8xdxWq
|
||||
Dh96n5US06lszEc+Lj3dqdxWkXRRqEbjhBFh/utXaeyeSOtaX65GwD5svDHnJBcl
|
||||
AGkzeRIXqxmYG+I2zMm/JYGzEnbwToyC7yF6Q8cQxOr37hEpqz+WN/x3qM2qyBLE
|
||||
CQFjmlJrvRLkSL15PCZiu+xFNFd/zx6btDun5DBlfDS9DG+SHCNH6Nq+NfP+ZQ8C
|
||||
GzP/3TaZPzMlKPDCjp0XOQfyQqFIXdwjPFTWjEusDBlm4qJAlQ==
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,8 @@
|
|||
class AddMetadataUriToAuthenticationProviders < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def change
|
||||
add_column :account_authorization_configs, :metadata_uri, :string
|
||||
add_index :account_authorization_configs, :metadata_uri, where: "metadata_uri IS NOT NULL"
|
||||
end
|
||||
end
|
|
@ -57,16 +57,17 @@ module CanvasHttp
|
|||
request = request_class.new(uri.request_uri, other_headers)
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
http.request(request) do |response|
|
||||
case response
|
||||
when Net::HTTPRedirection
|
||||
last_host = uri.host
|
||||
last_scheme = uri.scheme
|
||||
url_str = response['Location']
|
||||
redirect_limit -= 1
|
||||
else
|
||||
if response.is_a?(Net::HTTPRedirection) && !response.is_a?(Net::HTTPNotModified)
|
||||
last_host = uri.host
|
||||
last_scheme = uri.scheme
|
||||
url_str = response['Location']
|
||||
redirect_limit -= 1
|
||||
else
|
||||
if block_given?
|
||||
yield response
|
||||
else
|
||||
# have to read the body before we exit this block, and
|
||||
# close the connection
|
||||
response.body
|
||||
end
|
||||
return response
|
||||
|
|
|
@ -192,6 +192,7 @@ describe "AuthenticationProviders API", type: :request do
|
|||
@saml_hash['unknown_user_url'] = nil
|
||||
@saml_hash['parent_registration'] = false
|
||||
@saml_hash['jit_provisioning'] = false
|
||||
@saml_hash['metadata_uri'] = nil
|
||||
expect(json).to eq @saml_hash
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
#
|
||||
# Copyright (C) 2016 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_relative '../../../spec_helper'
|
||||
|
||||
describe AccountAuthorizationConfig::SAML::InCommon do
|
||||
let(:subject) { AccountAuthorizationConfig::SAML::InCommon }
|
||||
|
||||
describe ".refresh_providers" do
|
||||
before do
|
||||
AccountAuthorizationConfig::SAML.any_instance.stubs(:download_metadata).returns(nil)
|
||||
end
|
||||
|
||||
let!(:saml) { Account.default.authentication_providers.create!(auth_type: 'saml',
|
||||
metadata_uri: subject::URN,
|
||||
idp_entity_id: 'urn:mace:incommon:myschool.edu') }
|
||||
|
||||
it "does nothing if there aren't any InCommon providers" do
|
||||
saml.destroy
|
||||
subject.expects(:refresh_if_necessary).never
|
||||
subject.refresh_providers
|
||||
end
|
||||
|
||||
it "does nothing if no changes" do
|
||||
subject.expects(:refresh_if_necessary).returns(false)
|
||||
subject.expects(:validate_and_parse_metadata).never
|
||||
subject.refresh_providers
|
||||
end
|
||||
|
||||
it "records errors for missing metadata" do
|
||||
subject.expects(:refresh_if_necessary).returns('xml')
|
||||
subject.expects(:validate_and_parse_metadata).returns({})
|
||||
|
||||
Canvas::Errors.expects(:capture_exception).once
|
||||
saml.any_instantiation.expects(:populate_from_metadata).never
|
||||
|
||||
subject.refresh_providers
|
||||
end
|
||||
|
||||
it "continues after a failure" do
|
||||
saml2 = Account.default.authentication_providers.create!(auth_type: 'saml',
|
||||
metadata_uri: subject::URN,
|
||||
idp_entity_id: 'urn:mace:incommon:myschool2.edu')
|
||||
subject.expects(:refresh_if_necessary).returns('xml')
|
||||
subject.expects(:validate_and_parse_metadata).returns({
|
||||
'urn:mace:incommon:myschool.edu' => 'metadata1',
|
||||
'urn:mace:incommon:myschool2.edu' => 'metadata2',
|
||||
})
|
||||
|
||||
Canvas::Errors.expects(:capture_exception).once
|
||||
saml.any_instantiation.expects(:populate_from_metadata).with('metadata1').raises('error')
|
||||
saml2.any_instantiation.expects(:populate_from_metadata).with('metadata2')
|
||||
saml2.any_instantiation.expects(:save!).never
|
||||
|
||||
subject.refresh_providers
|
||||
end
|
||||
|
||||
it "populates and saves" do
|
||||
subject.expects(:refresh_if_necessary).returns('xml')
|
||||
subject.expects(:validate_and_parse_metadata).returns({
|
||||
'urn:mace:incommon:myschool.edu' => 'metadata1'
|
||||
})
|
||||
|
||||
saml.any_instantiation.expects(:populate_from_metadata).with('metadata1')
|
||||
saml.any_instantiation.expects(:changed?).returns(true)
|
||||
saml.any_instantiation.expects(:save!).once
|
||||
|
||||
subject.refresh_providers
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,112 @@
|
|||
#
|
||||
# Copyright (C) 2016 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_relative '../../../spec_helper'
|
||||
|
||||
describe AccountAuthorizationConfig::SAML::MetadataRefresher do
|
||||
let(:subject) { AccountAuthorizationConfig::SAML::MetadataRefresher }
|
||||
|
||||
describe ".refresh_providers" do
|
||||
before do
|
||||
AccountAuthorizationConfig::SAML.any_instance.stubs(:download_metadata).returns(nil)
|
||||
end
|
||||
|
||||
let (:saml1) { Account.default.authentication_providers.create!(auth_type: 'saml', metadata_uri: '1') }
|
||||
|
||||
it "keeps going even if one fails" do
|
||||
saml2 = Account.default.authentication_providers.create!(auth_type: 'saml', metadata_uri: '2')
|
||||
|
||||
subject.expects(:refresh_if_necessary).with(saml1.global_id, '1').raises('die')
|
||||
subject.expects(:refresh_if_necessary).with(saml2.global_id, '2').returns(false)
|
||||
::Canvas::Errors.expects(:capture_exception).once
|
||||
|
||||
subject.refresh_providers
|
||||
end
|
||||
|
||||
it "doesn't populate if nothing changed" do
|
||||
subject.expects(:refresh_if_necessary).with(saml1.global_id, '1').returns(false)
|
||||
saml1.expects(:populate_from_metadata_xml).never
|
||||
|
||||
subject.refresh_providers
|
||||
end
|
||||
|
||||
it "does populate, but doesn't save, if the XML changed, but nothing changes on the model" do
|
||||
subject.expects(:refresh_if_necessary).with(saml1.global_id, '1').returns('xml')
|
||||
saml1.any_instantiation.expects(:populate_from_metadata_xml).with('xml')
|
||||
saml1.any_instantiation.expects(:save!).never
|
||||
|
||||
subject.refresh_providers
|
||||
end
|
||||
|
||||
it "populates and saves" do
|
||||
subject.expects(:refresh_if_necessary).with(saml1.global_id, '1').returns('xml')
|
||||
saml1.any_instantiation.expects(:populate_from_metadata_xml).with('xml')
|
||||
saml1.any_instantiation.expects(:changed?).returns(true)
|
||||
saml1.any_instantiation.expects(:save!).once
|
||||
|
||||
subject.refresh_providers
|
||||
end
|
||||
end
|
||||
|
||||
describe ".refresh_if_necessary" do
|
||||
let(:redis) { stub("redis") }
|
||||
|
||||
before do
|
||||
Canvas.stubs(:redis_enabled?).returns(true)
|
||||
Canvas.stubs(:redis).returns(redis)
|
||||
end
|
||||
|
||||
it "passes ETag if we know it" do
|
||||
redis.expects(:get).returns("MyETag")
|
||||
CanvasHttp.expects(:get).with("url", "If-None-Match" => "MyETag")
|
||||
|
||||
subject.send(:refresh_if_necessary, 1, 'url')
|
||||
end
|
||||
|
||||
it "doesn't pass ETag if force_fetch: true" do
|
||||
redis.expects(:get).never
|
||||
CanvasHttp.expects(:get).with("url", {})
|
||||
|
||||
subject.send(:refresh_if_necessary, 1, 'url', force_fetch: true)
|
||||
end
|
||||
|
||||
it "returns false if not modified" do
|
||||
redis.expects(:get).returns("MyETag")
|
||||
response = stub("response")
|
||||
response.expects(:is_a?).with(Net::HTTPNotModified).returns(true)
|
||||
|
||||
CanvasHttp.expects(:get).with("url", "If-None-Match" => "MyETag").yields(response)
|
||||
|
||||
expect(subject.send(:refresh_if_necessary, 1, 'url')).to eq false
|
||||
end
|
||||
|
||||
it "sets the ETag if provided" do
|
||||
redis.expects(:get).returns(nil)
|
||||
response = stub("response")
|
||||
response.expects(:is_a?).with(Net::HTTPNotModified).returns(false)
|
||||
response.expects(:value)
|
||||
response.stubs(:[]).with('ETag').returns("NewETag")
|
||||
redis.expects(:set).with("saml_1_etag", "NewETag")
|
||||
response.expects(:body).returns("xml")
|
||||
|
||||
CanvasHttp.expects(:get).with("url", {}).yields(response)
|
||||
|
||||
expect(subject.send(:refresh_if_necessary, 1, 'url')).to eq "xml"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -110,6 +110,22 @@ describe AccountAuthorizationConfig::SAML do
|
|||
@aac = @account.authentication_providers.create!(:auth_type => "saml", :requested_authn_context => "anything")
|
||||
expect(@aac.requested_authn_context).to eq "anything"
|
||||
end
|
||||
|
||||
describe "download_metadata" do
|
||||
it 'requires an entity id for InCommon' do
|
||||
saml = Account.default.authentication_providers.new(auth_type: 'saml',
|
||||
metadata_uri: AccountAuthorizationConfig::SAML::InCommon::URN)
|
||||
expect(saml).not_to be_valid
|
||||
expect(saml.errors.first.first).to eq :idp_entity_id
|
||||
end
|
||||
|
||||
it 'changes InCommon URI to the URN for it' do
|
||||
saml = Account.default.authentication_providers.new(auth_type: 'saml',
|
||||
metadata_uri: AccountAuthorizationConfig::SAML::InCommon.endpoint)
|
||||
expect(saml).not_to be_valid
|
||||
expect(saml.metadata_uri).to eq AccountAuthorizationConfig::SAML::InCommon::URN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.resolve_saml_key_path' do
|
||||
|
|
Loading…
Reference in New Issue